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
CronJob = type=cron, script-path=/path/to/cron.js, cron-expression=*/30 * * * *
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. If the collected body exceeds this size, the script is skipped for that request (the body is not truncated) |
timeout |
No | 5.0 seconds | Per-script execution timeout. Values above 30 seconds are clamped to 30 |
argument |
No | — | Custom string argument available as $argument in JS |
debug |
No | false | Reserved; currently has no effect |
cron-expression |
No | — | Cron schedule expression (for cron type only) |
wake-system |
No | false | Reserved; parsed but currently has no effect |
enable |
No | true | Enable or disable this script |
script-update-interval |
No | 0 | Remote script update polling (see Execution Model) |
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. url must be returned together with at least one of status / headers / body; a result containing only url is treated as passthrough.
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)
The keys
server/serversare accepted but currently ignored; resolution falls through to normal DNS.
$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: null}
bssidis alwaysnull;ssidis only populated on iOS.Notice: In the current releases the
$networkand$environmentproperties are not visible to scripts due to a bridging limitation and read asundefined.$scriptmetadata is available in cron and event scripts only.
$environment — Runtime Info
$environment.system // "iOS" or "macOS"
$environment.appVersion // KLNEKit SDK 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.getPolicyGroups() // 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 // Monotonic timestamp (seconds since system boot reference, not Epoch)
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 tagged as a JavaScript error
setTimeout(fn, seconds) — Timer
Schedule a function to run after a delay.
setTimeout(function() {
console.log("Delayed execution")
}, 2.5) // 2.5 seconds
$scriptImport(subScriptPath) — Sub-script Loader
Load and evaluate another JavaScript file. Only local file paths are supported; http(s):// and file:// URLs are rejected. ($script is reserved for the script metadata object.)
$scriptImport("/path/to/helper.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]
HalfHourlyTask = type=cron, script-path=task.js, cron-expression=*/30 * * * *
Only the minute field of the expression is honored: */N runs every N minutes. Any other expression fires every 60 seconds, and the script must check the current time itself to decide whether to act.
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({})
}
)
Call
$done({})inside the callback — completing the script cancels all pending$httpClientrequests, so a synchronous$done()at the end of the script would cancel the health check before its response arrives.
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 pool of 3 pre-warmed JSContexts is kept; a context is discarded after each execution and replaced with a fresh one, so globals never leak between runs.
- On iOS/tvOS, scripts are subject to a 10MB memory limit; on macOS, 512MB.
- Remote scripts (HTTP/HTTPS paths) are fetched on every execution. When
script-update-intervalis non-zero, Chute additionally polls the URL with a conditional HEAD request every 10 minutes;0(the default) disables the polling.
Module Script Integration
Scripts can also be defined in Module files (.sgmodule) under the [Script] section.