Skip to content

Debugging tools

Scripting

Change requests and responses with JavaScript in Cloak Proxy. Full API reference for onRequest, onResponse, the request and response objects, sharedState, console and helpers.

Scripting lets you change matching requests and responses with JavaScript. A script defines onRequest, onResponse or both; Cloak Proxy calls them for every matching request and sends on whatever they return. Use it when a change needs logic: add a header from a saved token, rewrite part of a path, flip a field in a JSON response, or fail every third call.

  1. Right-click a request in the traffic list and choose Tools › Scripting….

    Cloak Proxy creates a rule that matches the request’s method and URL (without its query), fills in the starter script, turns Scripting on and opens the rule. To start from scratch, choose Tools › Edit Scripting Rules… (⌥ ⌘ S) and click +.

  2. Edit onRequest, onResponse or both in the editor. Remove the // from an example line to use it.

  3. Click Check Syntax. It shows No syntax errors or the first error with its line number.

  4. Make the request again in your app. Output from console.log appears in the Console section under the editor.

A new script. Both functions return what they receive, so nothing changes until you edit them.

  1. Choose which functions run.
  2. Compile the script and report syntax errors.
  3. Insert an example snippet.
  4. The script. No smart quotes or dashes; ⌘F finds text.
  5. console output from this script.
A Scripting rule for GET https://api.example.com/v1/users showing the starter script with onRequest and onResponse, Check Syntax and Snippets buttons, and an empty console
Setting Default What it does
Name and matcher Which requests the script runs on. See Rules and matching. The name also labels the script’s console output and errors.
Run onRequest On Calls onRequest before the request is sent.
Run onResponse On Calls onResponse before the response reaches the app.
Check Syntax Compiles the script without running it.
Snippets Appends an example to the end of the script.

The starter script:

Starter script
// Runs before the request is sent. Change anything and return it.
// request: { method, url, scheme, host, port, path, headers, queries, body }
function onRequest(context, url, request) {
// request.headers["X-Debug"] = "1";
// request.queries["page"] = "2";
return request;
}
// Runs before the response reaches the app.
// response: { statusCode, headers, body } — body is an object for JSON.
function onResponse(context, url, request, response) {
// response.body.debug = true;
// console.log("Status", response.statusCode);
return response;
}
function onRequest(context, url, request) { … }

Called before the request is sent, after any earlier matching scripts and before the request breakpoint, Map Local, Mock Sets and Map Remote.

Argument Type What it is
context object Information about the script and the request. See context.
url string The request’s full URL, the same as request.url.
request object The request. See The request object.

Return value: the request to send. You can change request in place and return it, or return nothing, and Cloak Proxy uses the changed request. If you return a different object, it replaces the request, so it must contain every field (see Return a new object).

If a script has no onRequest function, or Run onRequest is off, the request passes through unchanged.

function onResponse(context, url, request, response) { … }

Called when the response arrives, whether from the server, Map Local or a mock set. It runs after No Caching and before the response breakpoint.

Argument Type What it is
context object See context.
url string The URL the request was sent to, after any request scripts, breakpoint edits and Map Remote.
request object The request as it was sent, with the same fields as in onRequest. Changes to it here have no effect.
response object The response. See The response object.

Return value: the response for the app. Change response in place and return it (or return nothing), or return a complete replacement object.

If a script has no onResponse function, or Run onResponse is off, the response passes through unchanged.

Field Type Example What it is
context.scriptName string "Add auth header" The rule’s name.
context.flowID number 42 The request’s number in Cloak Proxy, the same one shown as Request #42 in the Breakpoint window.
context.clientApp string or null "Safari" The app that made the request, when Cloak Proxy knows it. Requests from Compose show "Cloak Proxy (Compose)"; from Repeat, "Cloak Proxy (Repeat)".
Field Type Example Notes
method string "POST" Sent in uppercase, whatever case you use.
url string "https://api.example.com/v1/users?page=2" The full URL. If you change it, it wins over every URL part below.
scheme string "https"
host string "api.example.com" Changing it doesn’t change the Host header.
port number 443 The default port when the URL has none (443 for HTTPS, 80 for HTTP). Must be a number. A default port is left out of the rebuilt URL.
path string "/v1/users" Percent-encoded, without the query string. A missing leading / is added.
headers object { "Accept": "application/json" } See Headers.
queries object { "page": "2" } Query parameters by name. See Query parameters.
body string or object { "name": "Ada Park" } See Bodies.
Field Type Example Notes
statusCode number 200 Set a new number to change the status. The standard reason phrase for the new code is sent.
headers object { "Content-Type": "application/json" } See Headers.
body string or object { "users": [] } See Bodies.
The body is body arrives as When you return it
JSON (the Content-Type contains json, such as application/json or application/problem+json) and parses An object or array It’s turned back into JSON text with JSON.stringify, so whitespace and formatting change.
JSON that doesn’t parse, or any other text A string Sent as the string.
Binary (not valid UTF-8), such as an image An empty string "" Leave it empty to keep the original bytes. Set a string to replace them.
Empty ""
  • Compressed bodies (gzip, deflate, Brotli) are decompressed before your script sees them, and the Content-Encoding header is removed, so the app receives the body uncompressed.
  • For a text or JSON body, setting body to null or undefined sends an empty body.
  • Setting body to an object on a response that wasn’t JSON sends JSON text, but the Content-Type header stays as it was. Set it too: response.headers["Content-Type"] = "application/json".

headers is a plain object from header name to value.

  • A header that appears more than once, such as Set-Cookie, is an array of strings: ["a=1; Path=/", "b=2; Path=/"]. Set an array to send a header several times.
  • To remove a header, use delete headers["X-Name"] or set it to null.
  • Values are sent as text: numbers become strings.
  • Content-Length isn’t in the object. Cloak Proxy sets it from the final body.
  • Cloak Proxy rebuilds the header list from the object, in alphabetical order by name.

request.queries maps each parameter name to its value as a string. A parameter without a value (?debug) has the value "". A name that appears more than once (?id=1&id=2) holds an array of its values (["1", "2"]), the same way repeated headers do.

If your script doesn’t change queries, the query string is sent exactly as the app wrote it: same order, repeats and encoding.

  • Change a value: request.queries["page"] = "2".
  • Send a parameter more than once: request.queries["id"] = ["1", "2"].
  • Add one: request.queries["debug"] = "1". New parameters are added after the existing ones, sorted by name.
  • Remove one: delete request.queries["debug"].
  • Existing parameters keep their order.

Change scheme, host, port, path or queries, and Cloak Proxy rebuilds the URL from them. Or set url to a whole new URL, which takes priority over the parts.

Point a request at staging
function onRequest(context, url, request) {
request.host = "staging.example.com"; // the Host header follows, unless you set it yourself
return request;
}

When you change host, a Host header that named the old host is updated to the new one (with the port, if it isn’t the default). If your script sets the Host header itself, your value is kept.

Changing the URL in a script doesn’t affect which other rules match: Block List, the request breakpoint, response scripts and the response breakpoint all match the URL the app sent. Map Local, Mock Sets and Map Remote see your new URL.

Returning the object you were given (or nothing) is the safest choice. If you return a new object, Cloak Proxy reads it like this:

Field missing from the returned object Result
method (request) or statusCode (response) The original is kept.
headers The original headers are kept. To remove a header, return headers without it, or set it to null.
body The body is empty.
url, scheme, host, port, path, queries (request) The original part is kept.

sharedState is one object shared by every script and every request. Use it to carry values between requests, such as a token from a login response.

  • It starts as {}.
  • Any script can read and write it.
  • It lasts until you click Reset sharedState in the Console section or quit Cloak Proxy. Editing scripts or turning Scripting off doesn’t clear it.

Each script also has its own scope. Variables declared at the top of a script (outside the two functions) keep their values between requests until you edit that script or reset. Other scripts can’t see them.

Use console to print values while a script runs.

Function Shown as
console.log(…) Normal text.
console.info(…) Same as console.log.
console.warn(…) Orange text.
console.error(…) Red text.

Arguments are joined with spaces. Objects and arrays are printed as JSON; other values as text.

The Console section under the editor shows the time and text of the last 300 messages from scripts with this rule’s name. The text is selectable. Scripts with the same name share a console, so give each script its own name.

Button What it does
Clear Console Removes this script’s messages.
Reset sharedState Empties sharedState, recompiles every script (resetting their top-level variables) and clears the console for all scripts.

Cloak Proxy keeps the last 2,000 messages from all scripts together. An AI assistant can read them with the script_logs MCP tool.

Function Returns
btoa(text) The Base64 encoding of text as UTF-8. Unlike the browser version, it accepts any Unicode text.
atob(base64) The decoded text, read as UTF-8. An empty string if the input isn’t valid Base64.
sha256(text) The SHA-256 hash of text (as UTF-8) as 64 lowercase hex characters.
md5(text) The MD5 hash of text (as UTF-8) as 32 lowercase hex characters.

Standard JavaScript built-ins such as JSON, Math, Date, RegExp, encodeURIComponent and decodeURIComponent are available too.

The Snippets menu appends an example to the end of your script. Each one is a commented hint: copy the lines into onRequest or onResponse and remove the //.

Snippet Inserts
Add a request header // request.headers["Authorization"] = "Bearer " + sharedState.token; (for onRequest)
Change a JSON response field // response.body.user.isPremium = true; (for onResponse)
Return a custom status // response.statusCode = 500; and // response.body = { error: "Simulated failure" }; (for onResponse)
Save a token for later requests // sharedState.token = response.body.access_token; (for onResponse of the login call)
Change a query parameter // request.queries["page"] = "2"; and // delete request.queries["debug"]; (for onRequest)
Point a request at another host // request.host = "staging.example.com"; and // request.scheme = "https"; (for onRequest)

Check Syntax compiles the script in a separate, empty environment without running it. It shows No syntax errors in green, or the error and its line number in red, for example SyntaxError: Unexpected token '}' (line 7). It doesn’t catch mistakes that only appear when the script runs, such as reading a field of undefined.

If a script throws an error, or doesn’t compile, the request fails instead of going through unchanged:

  • The app gets a 502 Bad Gateway response with the header X-Cloak-Proxy-Error: 1 and a plain-text body that names the script and the error, with its line number when there is one: Script “Add auth header”: TypeError: … (line 12).
  • The traffic list shows the request as failed with the same message. See Failed requests.
  • Nothing is written to the script’s console.

Guard against missing data so one odd response doesn’t break the app:

function onResponse(context, url, request, response) {
if (response.body && response.body.user) {
response.body.user.isPremium = true;
}
return response;
}
  • Scripts run in JavaScriptCore, the JavaScript engine built into macOS, not in Node.js or a browser. There’s no fetch, XMLHttpRequest, setTimeout, require, import or npm.
  • onRequest and onResponse must return their result right away. Promises and async functions aren’t awaited.
  • All scripts run one at a time on a single background thread. A slow script delays every request that has a script, and there’s no time limit: a loop that never ends stops all scripted traffic until you quit Cloak Proxy.
  • Scripts only see HTTP requests Cloak Proxy can read: plain HTTP and HTTPS with SSL Proxying on. They don’t run on WebSocket messages or encrypted tunnels.
  • Requests answered or dropped by the Block List, or dropped by Network Conditions packet loss, never reach scripts.

Every matching script runs, from the top of the list to the bottom, and each one receives the output of the one before. Which scripts match is decided once, from the URL the app sent, so a script that changes the URL doesn’t stop later scripts from running. The traffic list shows a Script badge on requests a script ran on.

Two scripts: one saves the token from the login response, the other adds it to every API request.

Save token: matches POST https://api.example.com/v1/login, Run onResponse only
function onResponse(context, url, request, response) {
if (response.statusCode === 200 && response.body && response.body.access_token) {
sharedState.token = response.body.access_token;
console.log("Saved a token from", context.clientApp);
}
return response;
}
Add token: matches api.example.com/v1/*, Run onRequest only
function onRequest(context, url, request) {
if (sharedState.token) {
request.headers["Authorization"] = "Bearer " + sharedState.token;
}
return request;
}
Flaky API
var count = 0;
function onResponse(context, url, request, response) {
count += 1;
if (count % 3 === 0) {
response.statusCode = 503;
response.headers["Retry-After"] = "5";
response.body = { error: "Service unavailable (simulated)" };
console.warn("Simulated 503 for request", context.flowID);
}
return response;
}

count resets when you edit the script or click Reset sharedState.

v1 to v2
function onRequest(context, url, request) {
request.path = request.path.replace(/^\/v1\//, "/v2/");
return request;
}
Request tweaks
function onRequest(context, url, request) {
delete request.headers["Cookie"];
var text = typeof request.body === "string" ? request.body : JSON.stringify(request.body);
request.headers["X-Body-SHA256"] = sha256(text);
return request;
}

A JSON body is sent as JSON.stringify(request.body), so the hash matches what the server receives.