Skip to content
Hoody.com

Hoody Exec’s @token magic comment adds shared-secret authentication to any script, covering both HTTP requests and WebSocket connections. You do not add middleware, an auth library, or JWT infrastructure. Put the comment in each script you want to protect: in a directory’s pre.ts or post.ts it has no effect.

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

Unauthenticated requests get 401 Unauthorized. Authenticated requests run your script normally.


  1. You add // @token <secret> at the top of your script.
  2. The server checks every incoming request except OPTIONS (see CORS preflight) for a matching token before your code runs.
  3. A matching token lets the script execute normally.
  4. A missing or wrong token returns 401 Unauthorized, and your code never runs. (A remote capabilities request is the one other exception: without a valid token it still answers 200, saying that a token is required and listing the names, never the secrets, of its valid @remote-token lines.)

Runs started by @schedule carry no token and are not checked. Firing a token-protected script by hand through the schedules API is refused with 403 unless the request passes force: true or the server was started with --schedules-trigger-allow-token true.

@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 (like every magic comment, it must sit in the first 30 lines, before any code; a @token further down is ignored and leaves the endpoint public), 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. When you edit a copy read back through the API, where tokens show as [REDACTED], the placeholders are filled back in by position, so deleting or reordering one can keep the wrong token; write the real values of the tokens you keep instead. A change applies to new requests and new WebSocket connections; a WebSocket that is already open stays connected until it closes. After any edit to the script, including a token change, connections opened before and after it no longer receive each other’s broadcasts, so reconnect clients that must share them. Remote sessions are the exception: they are checked again and closed once the script’s current tokens no longer admit them (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). The check runs about every 15 seconds, so a revoked session can stay open until then.

The token check happens at the server level, before the script’s context is built and its metadata constructed. There is no way for a script to see or override the gate.

@token also admits remote operations (event streams, send, call) that the script has switched on with @remote-messages or @remote-call, unless the script declares a @remote-token, which then replaces it for those operations. @token alone switches nothing on. It never unlocks remote eval.


A client can send the token four ways. The server checks them in the order below and uses the first usable one.

PriorityMethodHeader / Parameter
1aBearer tokenAuthorization: Bearer <token>
1bBasic auth (password field)Authorization: Basic base64(user:token)
2X-Token headerX-Token: <token>
3Query parameter?token=<token>

If multiple sources are present, the highest-priority usable one is used. For example, if both Authorization: Bearer and X-Token are sent, only the Bearer value is checked. An empty Bearer value, or a Basic value that does not decode to user:password or has an empty password, is skipped and the next method is tried. A non-empty wrong credential is not skipped, so it blocks the lower-priority ones. Remote operations differ: they try the credentials from every source (the Authorization value, the first X-Token, the first two ?token= values and WebSocket subprotocol tokens, up to 8 in all), so a wrong Bearer value does not hide a valid X-Token.


Bearer is the standard approach for API clients and SDKs.

Terminal window
curl -H "Authorization: Bearer my-secret-key-123" \
"https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/api/data"

The scheme name is case-insensitive (Bearer, bearer, BEARER all work).


The server matches the @token value against the password field of HTTP Basic auth. It ignores the username, so send anything or nothing.

That makes @token work directly with:

  • curl -u flag
  • HTTP clients that only support Basic auth
  • Legacy systems and webhook integrations
Terminal window
# Username "admin", password is the token; the username is ignored
curl -u admin:my-secret-key-123 \
"https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/api/data"

A custom header, useful when a proxy or gateway has already claimed Authorization.

Terminal window
curl -H "X-Token: my-secret-key-123" \
"https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/api/data"

Pass the token in the URL. This suits quick testing, webhook callbacks, and browser links.

Terminal window
curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/api/data?token=my-secret-key-123"

When authentication fails on an ordinary request, the server returns the response below. Remote operations answer in their own error format instead (REMOTE_TOKEN_REQUIRED or REMOTE_TOKEN_INVALID):

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="hoody-exec"
Content-Type: application/json
{
"error": "Unauthorized",
"message": "This endpoint requires authentication. Provide a valid token via Authorization: Bearer <token> header, X-Token header, ?token= query parameter, or HTTP Basic Auth."
}
  • The WWW-Authenticate header advertises Bearer. The server accepts Basic credentials when the client sends them, but this response does not trigger a browser username and password dialog.
  • The response body is the same whether the token is missing or wrong, so the reply leaks nothing about which one it was.
  • If the script configures CORS (@cors), the 401 response carries the CORS headers too.

The server answers every OPTIONS request with 204 before routing and the token check, whatever the script’s magic comments say, so a script like this one works with browser clients (a request a proxy hook sends to its target script is the exception: it is checked like any other method):

// @token my-secret-key-123
// @cors reflective
return { data: 'protected + CORS-enabled' };

Browsers send an OPTIONS preflight before a cross-origin request that needs one, such as any request with an Authorization or X-Token header (a plain GET with ?token= skips it and goes straight to the token check), and the HTTP spec does not let a preflight carry credentials. Requiring a token on the preflight would break CORS for every browser client.

The exchange runs in two steps:

  1. The browser sends OPTIONS with Origin and Access-Control-Request-Method, and the server returns 204 with the CORS headers. No token required.
  2. The browser sends the real GET or POST with Authorization: Bearer <token>, and the server checks the token before running the script.

This follows the CORS spec and is not a way around the gate: the request that reaches your code is still checked.


The @token gate also protects WebSocket connections. The token is checked when the connection is opened, before any of your script’s handlers run.

// @token my-secret-key-123
// @mode worker
// @websocket
ws.on('message', (socket, data) => socket.send('echo:' + data));

Send the token when you connect:

// Works in all WebSocket clients (including browsers)
// The WebSocket URL is the script's own route (file-based routing), not a dedicated /ws path
const ws = new WebSocket('wss://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/realtime/echo?token=my-secret-key-123');

Without a valid token, the connection opens and is closed at once with close code 4401 and reason Unauthorized. Handle the client’s close event to detect it. (A directory pre.js that refuses an upgrade works differently: that is a plain HTTP status with no handshake.)


Token comparison uses SHA-256 hashing on both sides followed by crypto.timingSafeEqual:

SHA-256(provided) === SHA-256(expected) // constant-time

This prevents timing attacks: the comparison takes the same amount of time regardless of how many characters match.

By default, Hoody Exec never returns the token in an API response or writes it to its logs:

SurfaceRedaction
Access logsThe ?token= value is never logged: the parameter is removed from the logged URL or its value replaced with [REDACTED]
Referer headers in logs?token= redacted
Scripts API (/api/v1/exec/scripts/read)Raw content shows // @token [REDACTED]
Magic Comments APItoken field returns [REDACTED]
metadata.parameters in your script?token= query param is removed before your code runs when it is the credential used
Encoded bypass attempts (%74oken=)Caught by URL-parser fallback and redacted

What your own script logs or returns (for example console.log(req.headers.authorization), or a request body logged in full mode) is recorded as is.

When a client authenticates with ?token=, the server strips the token from the request URL before your script runs, so your script never sees it. If the request authenticates with a higher-priority credential, such as a Bearer header, a ?token= parameter is not removed and stays visible to the script:

// @token my-secret
// Client calls: /api/data?token=my-secret&page=2
// Your script sees:
metadata.parameters // → { page: "2" } (no "token" key)
metadata.path // → /api/data (clean)

// @token sk_prod_a8f2e9c1d4b6
// @cors reflective
// `db` stands for your own database client; you provide it
const userId = metadata.parameters.id;
const user = await db.getUser(userId);
return user;
// @token whsec_github_abc123
if (req.method !== 'POST') {
res.statusCode = 405;
return { error: 'Method Not Allowed' };
}
// `processWebhook` stands for your own handler; you provide it
const payload = JSON.parse(req.rawBody.toString());
await processWebhook(payload);
return { received: true };
Terminal window
# GitHub webhook configured with:
# URL: https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/webhooks/github?token=whsec_github_abc123
// @token realtime-secret-456
// @mode worker
// @websocket
// @cors reflective
ws.on('message', (socket, data) => {
// Broadcast to this script's other clients. Do not keep sockets in `shared`:
// it is shared by every script of the instance, whatever their tokens.
ws.broadcast(data, socket);
});
Terminal window
# Read magic comments (token is redacted in response)
hoody exec magic comments get -c CONTAINER_ID --path "default/1/api/data.ts" -o json
# → { "path": "default/1/api/data.ts", "comments": { "token": "[REDACTED]", "tokens": ["[REDACTED]"], "cors": "reflective" } }