Skip to content
Hoody.com

Magic comments configure script behavior from the top of the file itself. You do not change code, write a configuration file, or set environment variables; you edit a comment and the behavior changes.

// @mode worker
// @cors reflective
// @timeout 5000
// @log-level standard
// @ai true
// Your code starts here...
return { hello: 'world' };

Magic comments are parsed at script load time. Put them at the very top of the file, before any code, imports, or expressions.

Only the first 30 lines of the file are scanned, and every line counts: a license header, a long JSDoc block, or a run of blank lines above your directives can push them out of the window, where they are ignored without an error. The window is published as parse_window_lines in the magic-comments schema. Keep the directives on the very first lines and put any prose header below them. The one exception is @return-type and @return-type-mode: they are read from comments anywhere in the file, so they still take effect below line 30.

Rules:

  • Start with // followed by a space and @
  • One comment per line
  • Comment names are case-sensitive
  • Most directives take everything after the name as the value; a few, such as @token, @mode and @log-level, read only the first word. Either way, never put a trailing comment on a directive line
  • A comment’s value overrides the default
  • The validation API parses and checks them

Validation example:

Terminal window
# Validate magic comments in a script
hoody exec magic comments validate \
--code $'// @mode worker\n// @cors reflective\n// @timeout 5000\nreturn {};'

CommentValuesDefaultDescription
@modeworker | serverlessserverlessExecution mode. Worker mode keeps one long-lived JavaScript context per instance, shared by all its worker scripts, so their globals are shared too; serverless mode builds a fresh context per request (functions registered for remote messages and calls keep the context of the run that registered them). The per-instance shared object persists in both (in memory: a restart or the eviction of an idle instance clears it), and neither is a security boundary
@enabledtrue | falsetrueEnable or disable script execution. A disabled script does not run. Setting it does not close script WebSockets already open; their handlers keep running. Remote streams and attach sockets close within about 15 seconds. It has no effect in pre.ts or post.ts, where no magic comment has any effect
@timeoutms | 90s/5m/2h | 0 | unlimited3600000Soft request timeout (default 1 hour). Bare number = ms; unit suffixes (s/m/h/d) accepted. Values above 2147483647 ms (about 24.8 days) are capped at it. Past the deadline an un-started response gets 504; the timeout does not interrupt an already-streaming response or kill in-flight script execution. 0/unlimited = no timeout. Requests from a proxy hook use the hook’s own timeout instead
@await-promisestrue | falsetrueAccepted so existing scripts keep working, but it has no effect. Returned promises are always awaited
@concurrentpositive integer | true | falsetrueMax concurrent executions (applies in both worker and serverless mode, to HTTP requests, cron, and remote send and call; WebSocket messages and remote eval are not held back by it). true = unlimited, false = single serial execution, N = at most N in parallel. Any other value is ignored with a warning
@schedulecron expression(none)Run the script on a cron schedule, always in UTC (e.g., 0 9 * * * runs at 09:00 UTC). Cannot be combined with @websocket. A change made through the scripts API applies at once; one made through the magic-comments API is applied in the background just after the call returns; after editing the file any other way, call POST /api/v1/exec/schedules/reload. See Scheduling
@schedule-timeoutms | 90s/5m/2h | 0 | unlimited@timeout, else 30000Per-fire wall-clock timeout for @schedule runs. HTTP requests keep @timeout; a scheduled run without @schedule-timeout uses the script’s @timeout, else 30 s. The value is read when the schedule is registered: after editing either directive directly, call POST /api/v1/exec/schedules/reload. Like @timeout it is soft: the fire is recorded as timed out and the next boundary can start, but the timed-out run is not killed (a returned stream or generator does stop being read at the deadline). It keeps running, keeps its @concurrent slot until it finishes, and still counts toward the limit of 10 unfinished fires per script (further fires are skipped past that limit)

Examples:

// One long-lived context, shared state
// @mode worker
// 60 second timeout
// @timeout 60000
// Process one request at a time
// @concurrent false

@timeout 0 and @timeout unlimited both mean no timeout. Use either with care. Two separate limits still apply: reading a request body (without @rawBody) must finish within 30 seconds or the request gets 408, and pre.ts before a WebSocket handshake gets at most 30 seconds before the upgrade is refused with 504. Separately, synchronous code that blocks the server for longer than its watchdog limit (about 30 seconds by default) gets the server restarted, which ends every running script and clears in-memory state such as shared.


CommentValuesDefaultDescription
@corsreflective | * | URL | nonereflectiveOrigin policy for the actual response. With no @cors* directive the server’s global reflective CORS applies; none strips CORS headers; reflective/URL set the allowed origin. * is an alias for reflective — the response always names the requesting origin, never a literal *
@cors-credentialstrue | falsefalseAllow credentials (cookies, auth headers). Works with any origin policy that names an origin, including @cors *, because that reflects the requesting origin rather than sending a literal *
@cors-methodsGET,POST,...(server default)Access-Control-Allow-Methods is preflight-only: the browser reads it from the OPTIONS response, which the server answers before the script runs, so the per-script value has no effect. The preflight allows the method the browser asked for (a default list if none was sent)
@cors-headersAuthorization,...(server default)Access-Control-Allow-Headers is preflight-only (answered before the script runs, so the per-script value has no effect there): the preflight allows the headers the browser asked for (a default list if none were sent). The per-script value drives Access-Control-Expose-Headers on the actual response
@cors-max-agenumber (seconds)(server default)Preflight cache hint. Not applied per-script: the preflight is answered before the script runs, with a fixed max age of 3600 seconds

Examples:

// Mirror request origin (development)
// @cors reflective
// Allow cookies
// @cors-credentials true

The other @cors values are * (any origin, for testing), a specific origin such as https://app.example.com (production), and none (strip CORS headers). Set exactly one @cors per script. The last one in the file wins, and @cors none also disables every @cors-* sub-directive below it, unless a later @cors <origin> reopens the config.


CommentValuesDefaultDescription
@log-levelnone | minimal | standard | full | debugstandardLogging verbosity
@debugtrue | falsefalseShortcut for @log-level debug
@log-request-bodyfull | redacted | off | true | falsefullLog incoming request bodies (not with @rawBody, where the server never reads the body itself). A value the parser does not recognise fails closed to redacted, not to the default
@log-response-bodyfull | redacted | off | true | falsefullLog response bodies. Same fail-closed rule as @log-request-body
@log-max-body-sizebytes | 512kb/1mb1048576Max body size to log. Bare number = bytes; unit suffixes (kb/mb/gb) accepted
@log-exclude-headersheader namesauthorization cookie x-tokenExtra headers to exclude from logs. The names you list are added to the three defaults, which you cannot switch off from a script
@log-retention-days0 to 365014Days to keep this script’s per-request logs (values above the 3650-day cap are clamped). Each log keeps the value in force when it was written, so a change applies to new logs only. Access, cron and remote logs are not affected
@debug-instrumenttrue | falsefalseEnable code instrumentation for detailed tracing. @log-level debug turns it on by itself, even with false; traces include variable values

Examples:

// Maximum logging, including code traces
// @log-level debug
// Log request payloads
// @log-request-body true
// Log response payloads
// @log-response-body true
// Also hide these headers
// @log-exclude-headers x-api-key x-session
// Keep logs for 30 days
// @log-retention-days 30
// Detailed code tracing (request logs show traces only at @log-level debug;
// WebSocket and remote runs write them to the server log at any level but none)
// @debug-instrument true

Log levels:

  • none: no per-request log (the access log still records the request)
  • minimal: the request line plus the final status and duration, without the detailed request and response logs (the request’s headers and query parameters are still saved, redacted, in its metadata, and console output is still logged)
  • standard: request metadata and headers (the default)
  • full: the most detailed level below debug; request and response logs match standard
  • debug: everything in full plus the script’s console.debug() output and automatic code-instrumentation traces

Body logging is controlled separately by @log-request-body and @log-response-body, not by the level. Every level except none can record request bodies, and every level except minimal and none can record response bodies. To keep payloads out of the logs, set both directives to off.


CommentValuesDefaultDescription
@aitrue | falsetrueEnable AI helpers. Injects model, openai, ai, generateText, streamText, and generateObject
@ai-modelmodel namehoody-ai/hoody-freeAI model to use. Overrides the server default (e.g., deepseek/deepseek-v4-pro)
@ai-temperature0 to 2(none)AI temperature parameter (provider default when unset)
@ai-max-tokenspositive integer(none)Max tokens for AI response (provider default when unset). Other values are ignored
@ai-keystringserver-configuredAI API key override (default uses the server’s --ai-key flag)

Example:

// @mode serverless
// @ai true
// @ai-model deepseek/deepseek-v4-pro
// @ai-temperature 0.3
const { text } = await ai.generate({
prompt: `Summarize this: ${req.body.content}`
});
return { summary: text };

@ai-temperature and @ai-max-tokens supply defaults for ai.generate, ai.stream, and ai.object. The top-level generateText, streamText, and generateObject do not apply them, so pass options explicitly when you use those.


CommentValuesDefaultDescription
@tokenstring(none)Per-endpoint shared secret. Requests must provide the token via one of four methods. @token takes one word: in // @token s3cret rotate monthly the token is s3cret and the rest of the line is ignored, with a warning. @token is repeatable: each line adds one accepted token, and a request that matches any of them passes. Remove a line to revoke that token, but keep at least one: with no @token line left, the endpoint is public. A change applies to new requests and new WebSocket connections; an open WebSocket stays connected, but like any script edit the change puts sockets opened before and after it into separate ws.broadcast sets (remote sessions are checked again about every 15 seconds, and closed if their credentials are no longer accepted; removing every @token when no @remote-token is declared makes remote messages and calls public, so open sessions stay; eval still needs an eval-scoped @remote-token)

Add @token to protect an endpoint, with no middleware, auth library, or configuration file involved:

// @token my-secret-key-123
// @cors reflective
return { data: 'only authenticated requests see this' };

Clients authenticate using any of these methods (checked in priority order):

PriorityMethodExample
1aAuthorization: Bearer <token>curl -H "Authorization: Bearer my-secret-key-123" ...
1bAuthorization: Basic (password field)curl -u user:my-secret-key-123 ... or curl -u :my-secret-key-123 ...
2X-Token headercurl -H "X-Token: my-secret-key-123" ...
3?token= query parametercurl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/api/report?token=my-secret-key-123"

Security details:

  • Constant-time comparison (SHA-256 + timingSafeEqual): immune to timing attacks
  • Hoody Exec redacts the token in its API responses and access logs ([REDACTED]), and by default in its per-request logs too. Anything your script logs or returns itself is recorded as is
  • When the request authenticates with ?token=, it is stripped from req.url and metadata.parameters before your script runs. If a higher-priority credential is used instead, a ?token= parameter stays visible to the script
  • CORS preflight (OPTIONS) returns 204 without requiring auth (spec-correct behavior), except for a request a proxy hook sends to its target script
  • WebSocket upgrade requests are also gated; pass the token via a header or ?token=
  • Empty or whitespace-only values are ignored (endpoint stays public)

See Authentication for the full guide with examples.


CommentValuesDefaultDescription
@remote-messages(flag)offAllow event streams (X-Hoody-Remote: events), send, and the hoody-remote.v1 attach socket on the script’s URL
@remote-call(flag)offAllow call of the functions the script exposes with remote.expose(name, fn)
@remote-eval(flag)offAllow eval in the script’s latest run. Also needs a @remote-token with the eval scope
@remote-token<name> <secret|env:NAME> [scope](none)Named token for remote operations. Repeatable, with a different name on each line: a second line with a name already used is ignored, so give a new token a new name when rotating. scope is a comma list of messages, call, eval (default messages,call). When any is declared, @token no longer admits remote operations
// @mode worker
// @remote-messages
// @remote-call
// @remote-token dashboard env:DASHBOARD_TOKEN messages
// @remote-token ops env:OPS_TOKEN messages,call

The three flags accept the bare directive, true or false; any other value is ignored, so the line has no effect (a flag that was off stays off). A @remote-token secret shorter than 16 characters matches nothing, and a @remote-token line whose values do not parse still counts as declared, so it locks remote operations rather than opening them. A misspelled comment name is not recognised at all and protects nothing. See Remote Messages and Events for the operations, frames and limits.


CommentValuesDefaultDescription
@websocket(flag)(none)Enable WebSocket support. Requires worker mode and cannot be combined with @schedule
@websocket-pretrue | falsetrueRun the directory’s pre.ts (or pre.js when there is no pre.ts) once before each WebSocket handshake. false opens the socket without it
@rawBody(flag)(none)Skip the request-body auto-parser, so req stays a raw Node Readable stream (for large uploads, streaming, or passthrough). Accepts @rawBody, @rawBody true, or @rawBody false

| @label | string | (none) | Script classification label for filtering and organization | | @return-type | TypeScript type | (none) | Type of the script’s return value, written inline: primitives, literals, object shapes, arrays, records, unions and interface bodies. Array<T>, Promise<T> (checked against the awaited value) and Record<K, V> work too. A named type such as User, other generics and tuples are not supported and fail validation | | @return-type-mode | strict | warn | dev | strict | Enforcement mode for @return-type validation. strict rejects on mismatch, warn logs the mismatch and continues. dev also rejects in every environment: outside production the client gets a 400 with the validation details, and in production an ordinary error, unless the script already sent its response (with res.json(), for example), in which case the client keeps what was sent. Validation runs only when the script returns a value other than null or undefined, in every mode. A type the server cannot parse, such as Record<string>, turns validation off (the error is only logged), even in strict mode | | @description | text | (none) | Description of the script, shown in script listings. The generated OpenAPI spec does not use it; describe the API in the script’s .openapi.json file instead | | @tags | Tag1, Tag2 | (none) | Tags for the script, used to filter script listings. The generated OpenAPI spec does not use them; set API tags in the script’s .openapi.json file instead |

Examples:

// @mode worker
// Enable WebSocket handlers
// @websocket
// Classify this script
// @label user-api
// @return-type { id: string, name: string, email: string }
// Enforce return-type at runtime
// @return-type-mode strict
// @description User profile API
// @tags Users, Profile

@websocket and @rawBody are the strictest of all: they are recognised only as the bare directive, or followed by exactly true or false. // @websocket // enable sockets and // @rawBody needed for uploads are both ignored outright, with no warning in the logs.


Manage magic comments through the API:

EndpointDescription
GET /api/v1/exec/magic-comments/schemaGet the schema of the magic comments this API manages: all except @rawBody, @return-type and @return-type-mode, which you edit in the script itself
GET /api/v1/exec/magic-comments/readRead magic comments from a script
PUT /api/v1/exec/magic-comments/updateUpdate magic comments on a script
POST /api/v1/exec/magic-comments/bulk-updateBulk update magic comments across multiple scripts
Terminal window
# Read magic comments from a specific script
hoody exec magic comments get --path "default/1/api/hello.ts"

All magic comments, by category:

CategoryComments
Execution@mode, @enabled, @timeout, @await-promises, @concurrent, @schedule, @schedule-timeout
Authentication@token
Remote operations@remote-messages, @remote-call, @remote-eval, @remote-token
CORS@cors, @cors-credentials, @cors-methods, @cors-headers, @cors-max-age
Logging@log-level, @debug, @log-request-body, @log-response-body, @log-max-body-size, @log-exclude-headers, @log-retention-days, @debug-instrument
AI@ai, @ai-model, @ai-temperature, @ai-max-tokens, @ai-key
Advanced@websocket, @websocket-pre, @rawBody, @label, @return-type, @return-type-mode, @description, @tags