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.
Write your first script
Section titled “Write your first script”-
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 +.
-
Edit
onRequest,onResponseor both in the editor. Remove the//from an example line to use it. -
Click Check Syntax. It shows No syntax errors or the first error with its line number.
-
Make the request again in your app. Output from
console.logappears in the Console section under the editor.
A new script. Both functions return what they receive, so nothing changes until you edit them.
- Choose which functions run.
- Compile the script and report syntax errors.
- Insert an example snippet.
- The script. No smart quotes or dashes; ⌘F finds text.
- console output from this script.
Rule settings
Section titled “Rule settings”| 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:
// 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;}onRequest
Section titled “onRequest”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.
onResponse
Section titled “onResponse”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.
context
Section titled “context”| 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)". |
The request object
Section titled “The request object”| 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. |
The response object
Section titled “The response object”| 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. |
Bodies
Section titled “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-Encodingheader is removed, so the app receives the body uncompressed. - For a text or JSON body, setting
bodytonullorundefinedsends an empty body. - Setting
bodyto an object on a response that wasn’t JSON sends JSON text, but theContent-Typeheader stays as it was. Set it too:response.headers["Content-Type"] = "application/json".
Headers
Section titled “Headers”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 tonull. - Values are sent as text: numbers become strings.
Content-Lengthisn’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.
Query parameters
Section titled “Query parameters”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 where a request goes
Section titled “Change where a request goes”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.
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.
Return a new object
Section titled “Return a new object”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
Section titled “sharedState”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.
console
Section titled “console”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.
Built-in functions
Section titled “Built-in functions”| 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.
Snippets
Section titled “Snippets”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
Section titled “Check syntax”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.
When a script fails
Section titled “When a script fails”If a script throws an error, or doesn’t compile, the request fails instead of going through unchanged:
- The app gets a
502 Bad Gatewayresponse with the headerX-Cloak-Proxy-Error: 1and 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;}Limits
Section titled “Limits”- Scripts run in JavaScriptCore, the JavaScript engine built into macOS, not in Node.js or a browser. There’s no
fetch,XMLHttpRequest,setTimeout,require,importor npm. onRequestandonResponsemust return their result right away. Promises andasyncfunctions 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.
Several scripts on one request
Section titled “Several scripts on one request”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.
Examples
Section titled “Examples”Log in once, reuse the token
Section titled “Log in once, reuse the token”Two scripts: one saves the token from the login response, the other adds it to every API request.
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;}function onRequest(context, url, request) { if (sharedState.token) { request.headers["Authorization"] = "Bearer " + sharedState.token; } return request;}Fail every third request
Section titled “Fail every third request”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.
Move an API version
Section titled “Move an API version”function onRequest(context, url, request) { request.path = request.path.replace(/^\/v1\//, "/v2/"); return request;}Hide a cookie and add a body hash
Section titled “Hide a cookie and add a body hash”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.