JavaScript Scripting
Chute supports JavaScript scripting for advanced request/response modification, custom rule matching, DNS resolution, and scheduled tasks. Scripts use Apple's JavaScriptCore engine and follow the Surge-compatible script API.
Scripts are defined in the [Script] section of the configuration file.
Configuration
[Script]
MyScript = type=http-request, script-path=/path/to/script.js, pattern=^https?://example\.com, requires-body=true, max-size=262144, timeout=10, argument=myArg, debug=true
CronJob = type=cron, script-path=/path/to/cron.js, cron-expression=* * * * *, wake-system=true
Script Parameters
| Parameter | Required | Default | Description |
|---|---|---|---|
type |
Yes | http-request |
Script trigger type (see below) |
script-path |
Yes | — | Local file path or HTTP(S) URL to the JS script |
pattern |
No | (match all) | URL regex pattern to filter when the script triggers |
requires-body |
No | auto-detect | Force script to receive full request/response body |
max-size |
No | 131072 (128KB) | Max body size in bytes for body-accessing scripts |
timeout |
No | 5.0 seconds | Per-script execution timeout |
argument |
No | — | Custom string argument available as $argument in JS |
debug |
No | false | Skip compilation cache for debugging |
cron-expression |
No | — | Cron schedule expression (for cron type only) |
wake-system |
No | false | Wake system to execute cron scripts (iOS only) |
enable |
No | true | Enable or disable this script |
script-update-interval |
No | 0 | Seconds between remote script updates (-1 = never, 0 = only on start) |
Script Types
| Type String | Enum | Description |
|---|---|---|
http-request |
HTTP Request | Intercept and modify HTTP requests before upstream |
http-response |
HTTP Response | Intercept and modify HTTP responses before client |
http-request-before-send |
HTTP Request Before Send | Modify request after full body collected, before sending |
rule |
Rule | Custom rule matching logic |
dns |
DNS | Custom DNS resolution |
cron |
Cron | Scheduled/timed scripts |
event |
Event | System event handlers (e.g., network-changed) |
Body auto-detection: If the script source contains
$request.bodyor$response.body, the body will automatically be provided up tomax-size. Userequires-body=trueto force this behavior.
JavaScript API Reference
Scripts run in a sandboxed JavaScriptCore environment with the following global objects available.
$request (Read-only)
Available in: http-request, http-response, http-request-before-send, rule
| Property | Type | Description |
|---|---|---|
.url |
String | Full request URL |
.method |
String | HTTP method (GET, POST, etc.) or QUERY for DNS |
.headers |
Object | Request headers as key-value pairs |
.body |
String or null | Request body (UTF-8 decoded) |
.hostname |
String | Target hostname |
.destPort |
Number | Destination port |
.processPath |
String | Requesting process path (macOS only) |
.userAgent |
String | User-Agent header value |
.sourceIP |
String | Source IP address |
.listenPort |
Number | Proxy listen port |
.requestId |
String | Unique request ID |
.dnsResult |
String | Resolved IP address |
.srcPort |
Number | Source port |
.protocol |
String | Detected protocol: http, https, tcp, dns |
$response (Read-only)
Available in: http-response
| Property | Type | Description |
|---|---|---|
.status |
Number | HTTP status code |
.headers |
Object | Response headers as key-value pairs |
.body |
String or null | Response body (UTF-8 decoded) |
$done(value) — Completion Handler
Must be called exactly once at the end of the script to signal completion. Script execution blocks until $done() is called or timeout expires.
$done({}) // Passthrough — no modifications
$done() // Abort the connection
$done({matched: true}) // Rule match result (rule scripts only)
$done({address: "1.2.3.4"}) // DNS result (dns scripts only)
HTTP Request script return values:
$done({
url: "https://new.example.com/path", // Rewrite URL
headers: {"X-Custom": "value"}, // Modify headers
body: "new request body", // Modify body
response: { // Return a synthetic response (skip upstream)
status: 200,
headers: {"Content-Type": "text/html"},
body: "<html>Blocked</html>"
}
})
When response is provided, the request is short-circuited: Chute returns the synthetic response directly to the client without contacting the upstream server. This is useful for blocking, mocking APIs, or returning cached content.
HTTP Response script return values:
$done({
status: 200, // Modify status code
headers: {"X-Custom": "value"}, // Modify response headers
body: "new response body", // Modify response body
url: "https://other.example.com" // Trigger 302 redirect
})
When url is provided, Chute returns a 302 redirect to the given URL instead of the original response.
DNS script return values:
$done({address: "1.2.3.4"}) // Single IP
$done({addresses: ["1.2.3.4", "5.6.7.8"]}) // Multiple IPs
$done({address: "10.0.0.1", ttl: 300}) // With custom TTL (seconds, default 60)
$done({server: "8.8.8.8"}) // Forward to specific DNS server
$httpClient — Async HTTP Client
Make HTTP requests from within scripts. All requests are cancelled on $done() or timeout.
$httpClient.get(url, function(error, response, data) {
if (error) {
console.log("Request failed: " + error)
} else {
console.log("Status: " + response.status)
console.log("Response: " + data)
}
})
$httpClient.post(url, {headers: {...}, body: "...", timeout: 5}, callback)
$httpClient.put(url, options, callback)
$httpClient.del(url, options, callback)
$httpClient.head(url, options, callback)
$httpClient.options(url, options, callback)
$httpClient.patch(url, options, callback)
Callback signature: callback(error, response, data)
error: Error string ornullresponse:{status: Number, headers: Object}ornulldata: UTF-8 string response body ornull
$persistentStore — Key-Value Storage
Persistent key-value storage that survives script and process restarts. Backed by NSUserDefaults.
$persistentStore.write(data, key) // Store a value
$persistentStore.read(key) // Retrieve a value
$persistentStore.remove(key) // Remove a value
$notification — Local Notifications
Post local system notifications.
$notification.post("Title", "Subtitle", "Notification body text")
$network — Network Info
Read-only network state information.
$network.dns // Array of DNS server IPs
$network.wifi // {ssid: "WiFiName", bssid: "aa:bb:cc:dd:ee:ff"}
$environment — Runtime Info
$environment.system // "iOS" or "macOS"
$environment.appVersion // App version string
$utils — Utilities
$utils.geoip("1.2.3.4") // Country code (e.g., "US")
$utils.ipasn("1.2.3.4") // ASN number (e.g., "13335")
$utils.ungzip(data) // Decompress gzip data
$klne — Proxy Control API
Control the proxy runtime from scripts.
$klne.policyGroups // Get all policy groups
$klne.selectPolicy("Group", "Proxy") // Switch policy for a group
$klne.getActiveConnections() // List active connections
$klne.closeConnection("id") // Close a connection
$klne.flushDNS() // Purge DNS cache
$klne.startURLTest("Group") // Trigger URL test for a group
$klne.reloadConfiguration() // Reload all configuration
$klne.setOutboundMode("rule") // Set mode: "global", "proxy", "direct", "rule"
$klne.setHTTPCaptureEnabled(true) // Enable/disable MITM
$script — Script Metadata
$script.name // Script name from config
$script.type // Script type string
$script.startTime // Epoch timestamp
Per-Execution Globals
The following variables are injected per script execution and are specific to certain script types.
$argument — Script Argument
The string value from the argument= parameter in the script configuration. Available in: http-request, http-response, http-request-before-send, rule, dns, cron.
console.log("Argument: " + $argument)
$domain — DNS Domain (DNS Script Only)
The domain name being queried. Available only in dns scripts.
var domain = $domain // e.g. "example.com"
$cronexp — Cron Expression (Cron Script Only)
The cron schedule expression from the script configuration. Available only in cron scripts.
console.log("Schedule: " + $cronexp) // e.g. "*/30 * * * *"
$event — Event Info (Event Script Only)
Information about the triggering event. Currently only network-changed is supported.
console.log("Event: " + $event.name) // "network-changed"
console — Logging
console.log("Debug message") // Verbose log
console.warn("Warning message") // Warning log
console.error("Error message") // Warning log with [JS-ERROR] prefix
setTimeout(fn, seconds) — Timer
Schedule a function to run after a delay.
setTimeout(function() {
console.log("Delayed execution")
}, 2.5) // 2.5 seconds
$script(subScriptPath) — Sub-script Loader
Load and evaluate another JavaScript file.
$script("/path/to/helper.js")
$script("https://example.com/remote-script.js")
// Pass data between scripts using $persistentStore
Script Type Details
HTTP Request Script
Executed when request headers are received. Can modify URL, headers, and body before the request is forwarded.
[Script]
ModifyHeaders = type=http-request, script-path=modify.js, pattern=^https://api\.example\.com
HTTP Response Script
Executed when response headers are received. Can modify status, headers, and body before returning to the client.
[Script]
ModifyResponse = type=http-response, script-path=response.js, pattern=^https://api\.example\.com
HTTP Request Before Send Script
Executed after the full request body has been collected, just before sending upstream. Useful for modifying POST/PUT request bodies.
[Script]
BeforeSend = type=http-request-before-send, script-path=before-send.js, pattern=^https://api\.example\.com, requires-body=true
Rule Script
Custom rule matching. The script must call $done({matched: true}) or $done({matched: false}).
[Rule]
SCRIPT,MyRuleScript,DIRECT
[Script]
MyRuleScript = type=rule, script-path=rule.js
DNS Script
Custom DNS resolution. Receives $domain and returns resolved address(es).
// dns.js
var domain = $domain
if (domain === "internal.example.com") {
$done({address: "10.0.0.1", ttl: 300})
} else {
$done({}) // Passthrough to normal DNS resolution
}
Cron Script
Scheduled execution using cron expressions. Minimum interval is 60 seconds.
[Script]
HourlyTask = type=cron, script-path=hourly.js, cron-expression=0 * * * *
Simplified cron syntax is supported (e.g., */30 * * * * for every 30 minutes, comma-separated values).
Event Script
Triggered by system events. Currently supports the network-changed event (fires when WiFi or cellular network changes).
[Script]
NetChange = type=event, script-path=network-changed.js
The $event object is available:
$event.name // "network-changed"
Practical Examples
Redirect Mobile Devices
An http-request script that redirects mobile users based on User-Agent:
[Script]
MobileRedirect = type=http-request, script-path=mobile-redirect.js, pattern=^https://example\\.com
// mobile-redirect.js
var ua = $request.headers["User-Agent"] || ""
if (/Mobile|Android|iPhone/.test(ua)) {
$done({
response: {
status: 302,
headers: {"Location": "https://m.example.com" + $request.url.replace(/.*example\\.com/, "")},
body: ""
}
})
} else {
$done({})
}
Block Content in API Responses
An http-response script that removes ads and sponsored content from a JSON API response:
[Script]
RemoveAds = type=http-response, script-path=remove-ads.js, pattern=^https://api\\.example\\.com/feed, requires-body=true
// remove-ads.js
var body = JSON.parse($response.body)
if (body.ads) {
delete body.ads
}
if (body.recommendations) {
body.recommendations = body.recommendations.filter(function(r) {
return !r.sponsored
})
}
$done({body: JSON.stringify(body)})
Modify Request Body Before Sending
An http-request-before-send script that sanitizes a POST payload:
[Script]
SanitizePayload = type=http-request-before-send, script-path=sanitize.js, pattern=^https://api\\.example\\.com/submit, requires-body=true
// sanitize.js
var body = JSON.parse($request.body)
body.clientSecret = "[REDACTED]"
body.timestamp = Math.floor(Date.now() / 1000)
$done({body: JSON.stringify(body)})
Custom Rule: Time-Based Routing
A rule script that selects a different proxy depending on the time of day:
[Rule]
SCRIPT,TimeBasedRule,ProxyA
[Script]
TimeBasedRule = type=rule, script-path=time-rule.js
// time-rule.js
var hour = new Date().getHours()
if (hour >= 9 && hour < 18) {
$done({matched: false}) // Fall through to next rule during work hours
} else {
$done({matched: true}) // Use ProxyA during off hours
}
Custom DNS for Internal Domains
A dns script that resolves internal hostnames to local IPs:
[Script]
InternalDNS = type=dns, script-path=internal-dns.js
// internal-dns.js
var internalHosts = {
"gitlab.local": "10.0.0.10",
"registry.local": "10.0.0.11",
"monitor.local": "10.0.0.12"
}
if (internalHosts[$domain]) {
$done({address: internalHosts[$domain], ttl: 3600})
} else {
$done({}) // Passthrough to normal DNS
}
Auto-Switch Policy on Network Change
An event script that switches to a conservative policy group when on cellular:
[Script]
NetSwitch = type=event, script-path=network-switch.js
// network-switch.js
if (!$network.wifi.ssid) {
// On cellular — use low-data group
$klne.selectPolicy("MainGroup", "LowDataProxy")
console.log("Switched to cellular profile")
} else if ($network.wifi.ssid === "Office") {
$klne.selectPolicy("MainGroup", "DIRECT")
console.log("Switched to office profile")
}
Periodic Health Check
A cron script that checks proxy health every 30 minutes:
[Script]
HealthCheck = type=cron, script-path=health-check.js, cron-expression=*/30 * * * *
// health-check.js
$httpClient.head("https://www.google.com/generate_204", {timeout: 10},
function(error, response, data) {
if (error || response.status !== 204) {
console.error("Health check failed: " + (error || "status " + response.status))
$notification.post("Chute Alert", "Health Check", "Cannot reach Google")
} else {
console.log("Health check OK")
}
}
)
$done()
Enrich API Responses with External Data
An http-response script that enriches user data by calling a secondary API:
[Script]
EnrichUsers = type=http-response, script-path=enrich.js, pattern=^https://api\\.example\\.com/users, requires-body=true
// enrich.js
var users = JSON.parse($response.body)
var pending = users.length
if (pending === 0) { $done({}) }
users.forEach(function(user, index) {
$httpClient.get("https://internal-api.example.com/avatar/" + user.id,
function(error, resp, data) {
if (!error && resp.status === 200) {
users[index].avatar = JSON.parse(data).url
}
pending--
if (pending === 0) {
$done({body: JSON.stringify(users)})
}
}
)
})
Execution Model
- All scripts run on a dedicated serial queue for thread safety.
- Each script execution has a per-script timeout; if
$done()is not called within the timeout, the script is treated as passthrough. - A JSContext pool (size 3) is used for performance; contexts are reused across executions.
- Script compilation is cached by default; use
debug=trueto bypass the cache. - On iOS/tvOS, scripts are subject to a 10MB memory limit; on macOS, 512MB.
- Remote scripts (HTTP/HTTPS paths) are fetched on start and optionally re-fetched at
script-update-intervalseconds.
Module Script Integration
Scripts can also be defined in Module files (.sgmodule) under the [Script] section.
```