Addons (Script Engine)
Addons let you write JavaScript scripts that process every HTTP request and response passing through Ghost. Think of them as programmable rules — you write a short script that runs automatically on every flow, and it can tag flows, modify headers, block requests, generate synthetic responses, or log information to a console.
For example: you could write a 5-line addon that tags every request to your payment API with “payment” so you can easily filter them later. Or an addon that adds a missing security header to every response so you can test what your app would look like with proper security headers. Or one that blocks all requests to analytics domains so they don’t clutter your traffic list.
Addons run in sandboxed JavaScript VMs powered by goja, a pure Go JavaScript engine. This means addons are fast, safe, and can’t crash Ghost — even if your script has a bug, it’s isolated in its own sandbox.
How Addons Work
Section titled “How Addons Work”When a request arrives at Ghost’s proxy, it passes through the interceptor pipeline — a chain of processing stages. The addon engine is one stage in this chain. For each flow, Ghost runs every enabled addon in priority order, giving each addon a chance to inspect and modify the request before it’s sent upstream, and then inspect and modify the response before it’s delivered back to the client.
What this diagram shows:
- A flow arrives at the proxy and enters the interceptor pipeline
- The addon engine’s
OnRequesthandler is called — it loops through every enabled addon (sorted by priority number, lowest first) and runs each addon’sonRequestcallback in its own sandboxed VM - Each addon can inspect the request and take an action: continue (do nothing), drop (block the request), respond (return a custom response without contacting the server), or modify (change headers, body, etc.)
- After the response comes back from the upstream server, the same process happens with
onResponse— each addon gets a chance to inspect and modify the response - Each addon runs in its own goja VM with its own state, so addons can’t interfere with each other
Writing an Addon
Section titled “Writing an Addon”Here’s a complete addon that tags API errors and logs a warning:
// Register a handler that runs when a RESPONSE comes backghost.onResponse(function(flow) { // Check if this is a server error (status 500+) if (flow.response.status_code >= 500) { // Add a tag so we can filter for these later flow.tag("server-error");
// Log a warning to the addon console ghost.warn("Server error on " + flow.request.url); }
// Check the response body for debug info that shouldn't be in production var body = flow.response.body; if (body && body.indexOf("stack trace") !== -1) { flow.tag("leaked-stacktrace"); ghost.error("Stack trace leaked in response!"); }});And here’s one that adds a custom header to every request:
ghost.onRequest(function(flow) { // Add a debug header to all requests flow.request.setHeader("X-Debug-Mode", "true");
// Tag requests to specific hosts if (flow.request.host === "api.hepsiburada.com") { flow.tag("hepsi-api"); }});API Surface
Section titled “API Surface”Flow Object
Section titled “Flow Object”Every handler receives a flow object with full access to the request and response data.
Top-level properties:
| Property | Type | Description |
|---|---|---|
flow.id | string | The unique flow ID (ULID) |
flow.session_id | string | Which session this flow belongs to |
flow.source | string | Where the flow came from: proxy, replay, script, or import |
Request properties (flow.request):
| Property | Type | Description |
|---|---|---|
flow.request.method | string | HTTP method: GET, POST, PUT, DELETE, etc. |
flow.request.url | string | The full request URL including query parameters |
flow.request.host | string | The hostname (e.g., api.example.com) |
flow.request.path | string | The URL path (e.g., /api/v1/users/123) |
flow.request.proto | string | The HTTP protocol version (e.g., HTTP/1.1, HTTP/2) |
flow.request.headers | object | Request headers as a key-value map (single value per key). Read-only — use setHeader/removeHeader to modify. |
flow.request.content_type | string | The Content-Type header value |
flow.request.body | string | The request body as a string |
Request mutation methods:
| Method | Description |
|---|---|
flow.request.setHeader(name, value) | Set or replace a request header |
flow.request.removeHeader(name) | Delete a request header |
flow.request.setBody(str) | Replace the entire request body |
Response properties (flow.response):
| Property | Type | Description |
|---|---|---|
flow.response.status_code | number | HTTP status code (200, 404, 500, etc.) |
flow.response.status_text | string | Status text (e.g., “OK”, “Not Found”) |
flow.response.headers | object | Response headers as a key-value map. Read-only — use setHeader/removeHeader to modify. |
flow.response.content_type | string | The Content-Type header value |
flow.response.body | string | The response body as a string |
Response mutation methods:
| Method | Description |
|---|---|
flow.response.setHeader(name, value) | Set or replace a response header |
flow.response.removeHeader(name) | Delete a response header |
flow.response.setBody(str) | Replace the entire response body |
Important: Response properties are only populated during onResponse handlers. In onRequest handlers, flow.response exists but all values are empty/zero and mutation methods are no-ops.
Flow Actions
Section titled “Flow Actions”| Method | Description |
|---|---|
flow.tag(name) | Add a string tag to this flow. Tags appear in the traffic list and are searchable via GQL (tag:name). Tagged flows are also exempt from noise detection suppression. |
flow.removeTag(name) | Remove a tag from this flow |
flow.annotate(key, value) | Add a key-value annotation (metadata) to the flow |
flow.drop() | Drop the flow entirely. The request is NOT forwarded to the upstream server — the client receives no response. Use with caution. Only works in onRequest. |
flow.respond(statusCode, headers, body) | Return a custom response without contacting the upstream server. The headers parameter is a key-value object. Only works in onRequest — calling this in onResponse does nothing. |
Global ghost Object
Section titled “Global ghost Object”| Property / Method | Description |
|---|---|
ghost.name | The name of this addon (read-only string) |
ghost.id | The ID of this addon (read-only string) |
ghost.onRequest(fn) | Register a function to be called when a request arrives. The function receives the flow object. |
ghost.onResponse(fn) | Register a function to be called when a response is received. The function receives the flow object. |
ghost.log(msg) | Write an info-level message to the addon’s log console |
ghost.warn(msg) | Write a warning-level message (displayed in amber) |
ghost.error(msg) | Write an error-level message (displayed in red) |
ghost.store | Key-value store for persisting data between handler invocations (see below) |
Key-Value Store (ghost.store)
Section titled “Key-Value Store (ghost.store)”Each addon has its own private key-value store that persists across handler invocations — meaning you can save a value in one request and read it in the next. This is useful for tracking state, counting occurrences, or caching decisions.
| Method | Description |
|---|---|
ghost.store.get(key) | Returns the stored value, or undefined if the key doesn’t exist |
ghost.store.set(key, value) | Stores a value under the given key. The value can be any JavaScript type (string, number, object, array, boolean). |
ghost.store.delete(key) | Removes a key from the store |
ghost.store.keys() | Returns an array of all stored key names |
Limits:
- Maximum 1,024 keys per addon. If the store is full and you try to add a new key, the
setis silently ignored and a warning is logged. - The store is in-memory only — data is lost when the addon is reloaded (saved) or Ghost restarts. This is by design: the store is for temporary runtime state, not permanent storage.
- No per-value size limit.
Example — counting API errors:
ghost.onResponse(function(flow) { if (flow.response.status_code >= 500) { var count = ghost.store.get("error_count") || 0; ghost.store.set("error_count", count + 1); ghost.log("Total server errors so far: " + (count + 1)); }});Sandbox Limits
Section titled “Sandbox Limits”Each addon runs in an isolated goja VM with strict safety limits to prevent runaway scripts from affecting Ghost’s performance:
| Limit | Value | What Happens If Exceeded |
|---|---|---|
| Execution timeout | 5 seconds per handler invocation | The VM is interrupted via vm.Interrupt(). The handler is aborted and Ghost continues processing the next addon. |
| Call stack depth | 512 frames | Deeply recursive functions are stopped to prevent stack overflow |
| Blocked globals | require, process, globalThis | These are removed from the VM environment — attempting to use them returns undefined. This prevents addons from accessing Node.js APIs or the global scope. |
Important clarifications:
- There is no explicit memory limit. The goja VM does not support memory caps. However, the 5-second timeout effectively limits how much memory a script can allocate.
- If a script panics (throws an unrecoverable error), Ghost catches it with
recover()— the addon is silently skipped and processing continues. Ghost never crashes due to an addon bug. - Each addon has its own
sync.Mutexensuring thread safety — only one handler can execute per addon at a time, even if multiple flows arrive simultaneously.
Addon Management
Section titled “Addon Management”Creating an Addon
Section titled “Creating an Addon”Click the “New” button in the Addons panel header to create a blank addon. Or click the dropdown arrow next to it to choose from 27 built-in templates organized by category (see Templates below).
Code Editor
Section titled “Code Editor”The addon editor provides:
- Editable name at the top (inline text input)
- Priority number — lower number = higher priority = runs first. When multiple addons are enabled, they execute in priority order.
- Code editor — a monospace text area for writing JavaScript. Tab key inserts 2-space indentation.
- Save button with keyboard shortcut Ctrl+S / Cmd+S. An “Unsaved” badge appears when you have unsaved changes.
- API Reference toggle — opens a side panel showing all available API methods organized by section (Lifecycle, Logging, Flow Actions, Request, Response, Flow Properties). Helpful when you forget a property name.
- Delete button — requires a double-click within 3 seconds to confirm (prevents accidental deletion)
Enable/Disable
Section titled “Enable/Disable”Each addon has a status toggle. Disabled addons are completely skipped during flow processing — they don’t consume any resources. The status is shown as a colored dot: green for active, grey for disabled.
Hot-Reload
Section titled “Hot-Reload”When you save an addon (Ctrl+S or click Save), Ghost immediately:
- Unloads the old version (removes the old VM)
- Creates a fresh goja VM
- Compiles and runs the new code
- Re-registers the addon in the execution pipeline
This happens instantly — no proxy restart needed. However, be aware that the in-memory store is cleared on reload (a new VM means a fresh state). The priority-sorted execution order is automatically recalculated.
If the new code fails to compile, the addon is left unloaded (the old version was already removed). A warning is logged but the save still succeeds in the database — you can fix the code and save again.
Built-in Templates
Section titled “Built-in Templates”Ghost ships with 27 addon templates across 5 categories. Each template is a working script you can use as-is or customize:
Logging (4 templates)
Section titled “Logging (4 templates)”| Template | What It Does |
|---|---|
| Request Logger | Logs every request’s method, URL, and status to the addon console |
| JSON API Monitor | Logs JSON API calls with response size and duration |
| Redirect Tracker | Logs HTTP redirect chains (301, 302, 307, 308 responses) |
| Cookie Inspector | Logs Set-Cookie headers to track cookie lifecycle |
Tagging (4 templates)
Section titled “Tagging (4 templates)”| Template | What It Does |
|---|---|
| Auto-Tag by Content Type | Tags flows based on response content type (json, html, image, etc.) |
| API Error Highlighter | Tags all 4xx and 5xx responses with error-type tags |
| GraphQL Operation Tagger | Parses GraphQL request bodies and tags with the operation name |
| Large Payload Flagger | Tags responses exceeding a configurable size threshold |
Security (10 templates)
Section titled “Security (10 templates)”| Template | What It Does |
|---|---|
| Block Tracker Domains | Drops requests to known advertising and tracking domains |
| Add Security Headers | Adds missing security headers (CSP, HSTS, X-Frame-Options) to responses |
| Strip Fingerprint Headers | Removes headers that reveal server technology (Server, X-Powered-By) |
| Sensitive Data Scanner | Scans response bodies for patterns that look like exposed credentials or PII |
| CORS Misconfiguration Detector | Flags responses with permissive or misconfigured CORS headers |
| Missing Security Headers | Tags responses missing recommended security headers |
| SQL Injection Pattern Detector | Tags responses containing SQL error messages (potential SQLi indicators) |
| Verbose Error Detector | Tags responses containing stack traces, debug info, or internal paths |
| Open Redirect Detector | Tags redirect responses pointing to external domains |
| Mixed Content Detector | Tags HTTPS pages that load resources over HTTP |
Testing (6 templates)
Section titled “Testing (6 templates)”| Template | What It Does |
|---|---|
| Mock API Endpoint | Intercepts requests to a specific URL and returns a mock response (using flow.respond()) |
| CORS Unblocker | Adds permissive CORS headers to all responses (useful for local development) |
| Auth Token Injector | Adds a configurable Authorization header to all requests to a target host |
| Contract Validator | Validates response JSON against an expected schema structure |
| Performance Budget | Tags flows exceeding configurable response size or duration thresholds |
| Response Consistency Checker | Detects inconsistent responses for the same endpoint across requests |
Modification (3 templates)
Section titled “Modification (3 templates)”| Template | What It Does |
|---|---|
| Response Body Rewriter | Replaces text patterns in response bodies (regex-based find/replace) |
| Request Header Injector | Adds custom headers to outgoing requests |
| Latency Simulator | Introduces artificial delay to responses for testing slow network conditions |
Addon Console
Section titled “Addon Console”Each addon has a dedicated log console visible at the bottom of the editor. Messages sent via ghost.log(), ghost.warn(), and ghost.error() appear here in real-time:
- Info messages (cyan) — from
ghost.log() - Warning messages (amber) — from
ghost.warn() - Error messages (red) — from
ghost.error()
Each entry shows a timestamp and the message text. The console keeps the last 500 log entries and auto-scrolls to the bottom when new messages arrive. Click the clear button to reset the console.
The console is filtered by the selected addon — each addon’s logs are separate.
Addon Events
Section titled “Addon Events”Addon lifecycle changes are broadcast to all connected frontends via WebSocket events:
| Event | When It Fires |
|---|---|
addon.created | A new addon was saved to the database |
addon.updated | An addon’s code or settings were changed |
addon.deleted | An addon was removed |
addon.log | An addon produced a log message (ghost.log/warn/error) |
These events keep the UI synchronized — if you have multiple Ghost windows open, creating an addon in one window immediately appears in the other.