Script management
Section titled “Script management”Use these endpoints to discover, read, write, organize, and inspect scripts on a Hoody container. Scripts live under /hoody/storage/hoody-exec/scripts inside the container. A path you send in a request is normally relative to the host’s instance namespace (for example, deploy.ts or jobs/clean.ts); the service adds the scope prefix. A path it returns in a response, such as resolvedPath, resolvedFrom, or resolvedTo, is relative to the scripts root, with the scope prefix attached (for example, default/1/deploy.ts). The scope prefix has the form <subdomain>/<execId>, where subdomain is the literal default whenever the request host carries no subdomain.
All script endpoints below live on the same per-container hostname:
https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.comExec ids
Section titled “Exec ids”GET /api/v1/exec/list
Section titled “GET /api/v1/exec/list”List the exec ids known to this container, with per-id file counts and a breakdown of SDK versus custom ids.
This endpoint takes no parameters.
curl -X GET "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/list" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.namespaces.list();{ "execIds": [ { "id": "1", "type": "sdk", "source_url": "https://example.com/sdk.zip", "files": 24 }, { "id": "2", "type": "custom", "files": 7 } ], "total": 2, "summary": { "sdk": 1, "custom": 1 }}Common fields on every entry under execIds:
| Field | Type | Required | Description |
|---|---|---|---|
execIds | array | Yes | One entry per exec id. |
execIds[].id | string | Yes | Id. |
execIds[].type | string | Yes | Type. One of: sdk, custom. |
execIds[].files | integer | Yes | Files. |
total | integer | Yes | Total count of exec ids. |
summary | object | Yes | Counts grouped by type. |
summary.sdk | integer | Yes | SDK ids. |
summary.custom | integer | Yes | Custom ids. |
When execIds[].type is sdk, entries may also carry:
| Field | Type | Required | Description |
|---|---|---|---|
execIds[].source_url | string | No | Source_url. |
execIds[].invalid | boolean | No | Present only when an SDK marker is unreadable; the entry then reports an empty file map. |
execIds[].error | string | No | Why the marker is invalid; present only with invalid. |
{ "error": "Malformed query string", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. | Percent-encoding failed to decode. Use valid %XX escapes. |
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: “Too many distinct clients”, “Too many requests”. |
{ "error": "Internal server error", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
Magic comments
Section titled “Magic comments”Magic comments are the leading @directive value lines at the top of a script that configure execution. The endpoints below let you read the schema, read a single script’s directives, update a single script, and bulk-update every script in a directory.
GET /api/v1/exec/magic-comments/read
Section titled “GET /api/v1/exec/magic-comments/read”Read the parsed magic comments from a script. Tokens are redacted.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
path | query | string | Yes | Path query parameter |
execId | query | string | No | Optional execution scope. When provided, relative paths resolve under default/{execId}/ unless subdomain is also set. Query value takes precedence over body. Lowercase alphanumeric, no hyphens — the -exec- part of a hostname cannot carry one, so a hyphenated execId names a tree no request could route to and is rejected with 400. |
exec_id | query | string | No | Alias for execId (snake_case). Lowercase alphanumeric, no hyphens — the -exec- part of a hostname cannot carry one, so a hyphenated execId names a tree no request could route to and is rejected with 400. |
subdomain | query | string | No | Optional subdomain namespace used with execId for path resolution. |
curl -X GET "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/magic-comments/read?path=deploy.ts" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.magicComments.get({ path: 'deploy.ts' });{ "path": "deploy.ts", "resolvedPath": "default/1/deploy.ts", "comments": { "enabled": true, "mode": "serverless", "timeout": "30s" }, "warnings": [ { "directive": "@timeout", "value": "30 banana", "message": "Unknown unit. The number is read as 30 milliseconds; the rest is ignored." } ]}| Field | Type | Required | Description |
|---|---|---|---|
path | string | Yes | The path the request named. |
resolvedPath | string | Yes | The file read, relative to the scripts directory: the path under the call’s scope (<subdomain|default>/<execId>/<path>) when that file exists, else the path as given. |
comments | object | Yes | Parsed magic comments (token and ai-key redacted). Keys vary per script. A remote-token entry that can never match carries invalid with the reason: a line that does not parse, or a literal secret that is too short. |
warnings | array | Yes | Directives whose value was present but unusable: a number with an unknown unit, trailing garbage or out of range, a value outside a directive’s allowed set, a @cors value that is not reflective, *, none or one origin, a @cors-methods / @cors-headers value that is not a comma-separated list of names, an @ai-model / @ai-key value with spaces or control characters, or a @remote-token that can never match. Also listed: an origin read without its trailing /, and a directive set again on a later line to another value. |
warnings[].directive | string | Yes | Directive name, for example @timeout. |
warnings[].value | string | Yes | The value as written (a secret shown as [REDACTED]). |
warnings[].message | string | Yes | Why it is unusable and what applies instead. |
{ "error": "path parameter is required", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. Messages: “path parameter is required”, “path is a directory; use magic-comments/bulk-update”, “Malformed query string” (Percent-encoding failed to decode. Use valid %XX escapes.). Other messages on this status are composed at runtime and are not listed here. |
details.code, when present, is one of: INVALID_PATH, IS_A_DIRECTORY.
{ "error": "Path is excluded from access", "code": "ERROR_403", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_403 | Forbidden | The service answers 403 with this code; the message names the cause. Messages: “Path is excluded from access”, “Invalid path: must be within scripts directory”, “Invalid path: resolves through a symlink”. Other messages on this status are composed at runtime and are not listed here. |
{ "error": "Script not found", "code": "ERROR_404", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_404 | Not found | The service answers 404 with this code; the message names the cause. |
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: “Too many distinct clients”, “Too many requests”. |
{ "error": "Internal server error", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
GET /api/v1/exec/magic-comments/schema
Section titled “GET /api/v1/exec/magic-comments/schema”Return the canonical magic-comments schema: categories, fields, and defaults.
This endpoint takes no parameters.
curl -X GET "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/magic-comments/schema" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.magicComments.getSchema();{ "schema_version": "1.0.0", "total_fields": 24, "parse_window_lines": 50, "unknown_keys_behavior": "ignored", "defaults_context": { "when_omitted": "none", "runtime": "service", "sdk_import": "compile-time" }, "categories": { "auth": ["token"], "remote": ["remote-messages", "remote-call", "remote-eval"], "execution": ["enabled", "mode", "timeout"], "logging": ["log-level", "log-request-body"], "cors": ["cors", "cors-credentials"], "ai": ["ai", "ai-model"], "metadata": ["description", "tags"] }, "fields": [ { "key": "enabled", "directive": "@enabled", "category": "execution", "type": "boolean", "description": "Enable or disable script execution.", "defaults": { "when_omitted": true, "runtime": true, "sdk_import": true } } ]}| Field | Type | Required | Description |
|---|---|---|---|
schema_version | string | Yes | Schema version. |
total_fields | integer | Yes | Number of fields described. |
parse_window_lines | integer | Yes | Lines scanned from the top of each script. |
unknown_keys_behavior | string | Yes | How unknown directive keys are handled. |
defaults_context | object | Yes | Context in which defaults apply. |
defaults_context.when_omitted | string | Yes | |
defaults_context.runtime | string | Yes | |
defaults_context.sdk_import | string | Yes | |
categories | object | Yes | Grouping of fields by category. |
categories.auth | array | Yes | |
categories.remote | array | Yes | |
categories.execution | array | Yes | |
categories.logging | array | Yes | |
categories.cors | array | Yes | |
categories.ai | array | Yes | |
categories.metadata | array | Yes | |
fields | array | Yes | Per-directive metadata. |
fields[].key | string | Yes | |
fields[].directive | string | Yes | |
fields[].category | string | Yes | |
fields[].type | string | Yes | |
fields[].enum_values | array | No | |
fields[].accepts_keywords | array | No | |
fields[].range | object | No | |
fields[].range.min | number | No | |
fields[].range.max | number | No | |
fields[].description | string | Yes | |
fields[].defaults | object | Yes | |
fields[].defaults.when_omitted | string | number | boolean | array | object | null | Yes | |
fields[].defaults.runtime | string | number | boolean | array | object | null | Yes | |
fields[].defaults.sdk_import | string | number | boolean | array | object | null | Yes | |
fields[].examples | array | No | |
fields[].notes | array | No |
{ "error": "Malformed query string", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. | Percent-encoding failed to decode. Use valid %XX escapes. |
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: “Too many distinct clients”, “Too many requests”. |
{ "error": "Internal server error", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
PUT /api/v1/exec/magic-comments/update
Section titled “PUT /api/v1/exec/magic-comments/update”Set directives on a single script. Keys that are not directives are ignored. Directives not named are left as they are. A value of null removes the directive.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
execId | query | string | No | Optional execution scope. When provided, relative paths resolve under default/{execId}/ unless subdomain is also set. Query value takes precedence over body. Lowercase alphanumeric, no hyphens — the -exec- part of a hostname cannot carry one, so a hyphenated execId names a tree no request could route to and is rejected with 400. |
exec_id | query | string | No | Alias for execId (snake_case). Lowercase alphanumeric, no hyphens — the -exec- part of a hostname cannot carry one, so a hyphenated execId names a tree no request could route to and is rejected with 400. |
subdomain | query | string | No | Optional subdomain namespace used with execId for path resolution. |
Request Body
Section titled “Request Body”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
path | string | Yes | Script to edit, relative to the scripts directory. | |
comments | object | Yes | Directives to set, keyed by directive name. | |
comments.enabled | boolean | No | Enable or disable script execution. | |
comments.token | string | array<string> | No | Shared secret token for endpoint authentication. A string sets one token; an array of up to 16 different tokens sets the whole list. | |
comments.mode | string | No | Execution model for the script. One of: worker, serverless. | |
comments.timeout | integer | string | No | Script timeout. Bare number is milliseconds; suffix (ms, s, m, h, d) honoured. | |
comments.log-level | string | No | Logging verbosity. One of: none, minimal, standard, full, debug. | |
comments.debug | boolean | No | Shortcut to set log level to debug. | |
comments.log-request-body | boolean | string | No | Include request body in logs. Boolean shorthand or full | redacted | off. | |
comments.log-response-body | boolean | string | No | Include response body in logs. Boolean shorthand or full | redacted | off. | |
comments.log-max-body-size | integer | string | No | Maximum body size to retain in logs. | |
comments.log-exclude-headers | array | No | Headers to redact from logs. | |
comments.log-retention-days | integer | No | Log retention window in days (0 to 3650). | |
comments.debug-instrument | boolean | No | Enable runtime debug instrumentation. | |
comments.await-promises | boolean | No | No effect: a returned promise is always awaited and its resolved value is the response. Accepted so existing scripts keep working. | |
comments.concurrent | boolean | integer | No | Concurrency policy: true, false, or a max concurrency integer. | |
comments.cors | string | No | CORS origin strategy. none (or null) writes @cors none; empty removes the directive. | |
comments.cors-credentials | boolean | No | Enable CORS credentials. | |
comments.cors-methods | string | No | Comma-separated CORS allowed methods. | |
comments.cors-headers | string | No | Comma-separated CORS allowed headers. | |
comments.cors-max-age | integer | No | CORS preflight max-age in seconds. | |
comments.websocket | boolean | No | Enable WebSocket mode for the script. | |
comments.ai | boolean | No | Enable or disable AI helpers. | |
comments.ai-model | string | No | Override default AI model identifier. | |
comments.ai-temperature | number | No | AI temperature between 0 and 2. Send 0 or a number from 0.000001 to 2. | |
comments.ai-max-tokens | integer | No | Maximum AI output tokens. | |
comments.ai-key | string | No | Override AI API key for the script. | |
comments.description | string | No | Human-readable description for the script. | |
comments.tags | array | No | Tags used for grouping and filtering. | |
comments.label | string | No | Short label used for classification or filtering. | |
comments.schedule | string | No | Cron expression or Bun nickname. UTC only. | |
comments.schedule-timeout | integer | string | No | Per-fire wall-clock timeout. | |
dry_run | boolean | No | false | When true, the file is not written. |
curl -X PUT "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/magic-comments/update" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "path": "deploy.ts", "comments": { "enabled": true, "timeout": "90s", "log-level": "standard" } }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.magicComments.update({ path: 'deploy.ts', comments: { enabled: true, timeout: '90s', 'log-level': 'standard' }});The response is one of two shapes, distinguished by the message field.
Applied:
{ "dry_run": false, "path": "default/1/deploy.ts", "resolvedPath": "default/1/deploy.ts", "previous": { "enabled": true, "timeout": "30s" }, "updated": { "enabled": true, "timeout": "90s", "log-level": "standard" }, "ignored": [], "message": "Magic comments updated successfully"}Preview only:
{ "dry_run": true, "path": "default/1/deploy.ts", "resolvedPath": "default/1/deploy.ts", "current": { "enabled": true, "timeout": "30s" }, "proposed": { "enabled": true, "timeout": "90s", "log-level": "standard" }, "changes": ["timeout", "log-level"], "ignored": [], "message": "Preview only - set dry_run=false to apply changes"}Applied shape (message: "Magic comments updated successfully"):
| Field | Type | Required | Description |
|---|---|---|---|
dry_run | boolean | Yes | True when no file was written. |
path | string | Yes | Scripts-root-relative path of the edited script. |
resolvedPath | string | Yes | The file read or edited, relative to the scripts directory: the path under the call’s scope (<subdomain|default>/<execId>/<path>) when that file exists, else the path as given. |
previous | object | Yes | Comments as they were before the edit. |
updated | object | Yes | Comments as they are after the edit. |
ignored | array | Yes | Keys of comments that are not directives (nor tokens): not written, and not counted in changes. Empty when every key is a directive. |
message | string | Yes | One of: Magic comments updated successfully. |
Preview shape (message: "Preview only - set dry_run=false to apply changes"):
| Field | Type | Required | Description |
|---|---|---|---|
dry_run | boolean | Yes | True when no file was written. |
path | string | Yes | Scripts-root-relative path of the edited script. |
resolvedPath | string | Yes | The file read or edited, relative to the scripts directory: the path under the call’s scope (<subdomain|default>/<execId>/<path>) when that file exists, else the path as given. |
current | object | Yes | Comments currently on the file. |
proposed | object | Yes | Comments that would be written. |
changes | array | Yes | Names of directives that would change. |
ignored | array | Yes | Keys of comments that are not directives (nor tokens): not written, and not counted in changes. Empty when every key is a directive. |
message | string | Yes | One of: Preview only - set dry_run=false to apply changes. |
{ "error": "path is required", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. Messages: “path is required”, “comments object is required”, “path is a directory; use magic-comments/bulk-update”, “Malformed query string” (Percent-encoding failed to decode. Use valid %XX escapes.), “Invalid JSON body” (Ensure the request body is valid JSON. Example: a backslash followed by a backtick is not a valid JSON escape.), “Request aborted before the body was fully received”. Other messages on this status are composed at runtime and are not listed here. |
details.code, when present, is one of: INVALID_PATH, IS_A_DIRECTORY.
{ "error": "Path is excluded from access", "code": "ERROR_403", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_403 | Forbidden | The service answers 403 with this code; the message names the cause. Messages: “Path is excluded from access”, “Invalid path: must be within scripts directory”. Other messages on this status are composed at runtime and are not listed here. |
{ "error": "Script not found", "code": "ERROR_404", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_404 | Not found | The service answers 404 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
{ "error": "Request body read timeout", "code": "ERROR_408", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_408 | Request timeout | The service answers 408 with this code; the message names the cause. |
{ "error": "Payload too large", "code": "ERROR_413", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_413 | Payload too large | The service answers 413 with this code; the message names the cause. |
{ "error": "Script validation failed", "code": "ERROR_422", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
The code field for 422 carries the value ERROR_422. The spec provides no further error-code table for this status. |
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: “Too many distinct clients”, “Too many requests”. |
{ "error": "Internal server error", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
POST /api/v1/exec/magic-comments/bulk-update
Section titled “POST /api/v1/exec/magic-comments/bulk-update”Set the same directives on every script in a directory. With dry_run: true, the response lists every matching file under would_affect with its current and proposed comments. With dry_run: false, the change is applied to every matching file and the response still succeeds when some fail: check results.failed before assuming every file changed.
Request Body
Section titled “Request Body”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
directory | string | No | Directory to edit, relative to the scripts directory. Ignored when execId is given. | |
execId | string | No | Edits default/<execId> (or <subdomain>/<execId> when taken from the Host). | |
comments | object | Yes | Directives to set, keyed by directive name. | |
comments.enabled | boolean | No | Enable or disable script execution. | |
comments.token | string | array<string> | No | Shared secret token for endpoint authentication. A string sets one token; an array of up to 16 different tokens sets the whole list. | |
comments.mode | string | No | Execution model for the script. One of: worker, serverless. | |
comments.timeout | integer | string | No | Script timeout. | |
comments.log-level | string | No | Logging verbosity. One of: none, minimal, standard, full, debug. | |
comments.debug | boolean | No | Shortcut to set log level to debug. | |
comments.log-request-body | boolean | string | No | Include request body in logs. | |
comments.log-response-body | boolean | string | No | Include response body in logs. | |
comments.log-max-body-size | integer | string | No | Maximum body size to retain in logs. | |
comments.log-exclude-headers | array | No | Headers to redact from logs. | |
comments.log-retention-days | integer | No | Log retention window in days (0 to 3650). | |
comments.debug-instrument | boolean | No | Enable runtime debug instrumentation. | |
comments.await-promises | boolean | No | No effect: a returned promise is always awaited and its resolved value is the response. Accepted so existing scripts keep working. | |
comments.concurrent | boolean | integer | No | Concurrency policy: true, false, or a max concurrency integer. | |
comments.cors | string | No | CORS origin strategy. | |
comments.cors-credentials | boolean | No | Enable CORS credentials. | |
comments.cors-methods | string | No | Comma-separated CORS allowed methods. | |
comments.cors-headers | string | No | Comma-separated CORS allowed headers. | |
comments.cors-max-age | integer | No | CORS preflight max-age in seconds. | |
comments.websocket | boolean | No | Enable WebSocket mode for the script. | |
comments.ai | boolean | No | Enable or disable AI helpers. | |
comments.ai-model | string | No | Override default AI model identifier. | |
comments.ai-temperature | number | No | AI temperature between 0 and 2. | |
comments.ai-max-tokens | integer | No | Maximum AI output tokens. | |
comments.ai-key | string | No | Override AI API key for the script. | |
comments.description | string | No | Human-readable description for the script. | |
comments.tags | array | No | Tags used for grouping and filtering. | |
comments.label | string | No | Short label used for classification or filtering. | |
comments.schedule | string | No | Cron expression or Bun nickname. UTC only. | |
comments.schedule-timeout | integer | string | No | Per-fire wall-clock timeout. | |
extension | string | No | ".ts" | File extension to match. |
recursive | boolean | No | true | Recurse into subdirectories. |
dry_run | boolean | No | false | When true, no file is written. |
curl -X POST "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/magic-comments/bulk-update" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "execId": "1", "comments": { "log-level": "standard", "timeout": "60s" }, "dry_run": false }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.magicComments.updateMany({ execId: '1', comments: { 'log-level': 'standard', timeout: '60s' }, dry_run: false});The response is one of two shapes, distinguished by the message field.
Preview only:
{ "dry_run": true, "directory": "default/1", "execId": "1", "recursive": true, "comments": { "log-level": "standard", "timeout": "60s" }, "ignored": [], "would_affect": { "total": 2, "files": [ { "file": "default/1/deploy.ts", "current": { "log-level": "minimal", "timeout": "30s" }, "proposed": { "log-level": "standard", "timeout": "60s" }, "changes": ["log-level", "timeout"] } ] }, "message": "Preview only - set dry_run=false to apply changes"}Applied:
{ "dry_run": false, "directory": "default/1", "execId": "1", "recursive": true, "comments": { "log-level": "standard", "timeout": "60s" }, "results": { "total": 2, "updated": 1, "failed": 1, "files": { "updated": ["default/1/deploy.ts"], "failed": [ { "file": "default/1/jobs/clean.ts", "error": "permission denied" } ] } }, "ignored": [], "message": "Magic comments updated successfully"}Preview shape (message: "Preview only - set dry_run=false to apply changes"):
| Field | Type | Required | Description |
|---|---|---|---|
dry_run | boolean | Yes | True when no file was written. |
directory | string | Yes | Resolved target directory (scripts-root-relative). |
execId | string | Yes | Exec id the bulk update targeted. |
recursive | boolean | Yes | Whether subdirectories were traversed. Default: true. |
comments | object | Yes | Directives applied. |
ignored | array | Yes | Keys of comments that are not directives (nor tokens): not written, and not counted in changes. Empty when every key is a directive. |
would_affect | object | Yes | Preview-only summary. |
would_affect.total | integer | Yes | Number of files that would be touched. |
would_affect.files | array | Yes | Per-file preview or error. |
message | string | Yes | One of: Preview only - set dry_run=false to apply changes. |
Applied shape (message: "Magic comments updated successfully"):
| Field | Type | Required | Description |
|---|---|---|---|
dry_run | boolean | Yes | True when no file was written. |
directory | string | Yes | Resolved target directory (scripts-root-relative). |
execId | string | Yes | Exec id the bulk update targeted. |
recursive | boolean | Yes | Whether subdirectories were traversed. Default: true. |
comments | object | Yes | Directives applied. |
results | object | Yes | Apply summary. |
results.total | integer | Yes | Number of files attempted. |
results.updated | integer | Yes | Number of files written. |
results.failed | integer | Yes | Number of files that could not be written. |
results.files | object | Yes | |
results.files.updated | array | Yes | Paths of files that were written. |
results.files.failed | array | Yes | Per-file failure records. |
results.files.failed[].file | string | Yes | |
results.files.failed[].error | string | Yes | |
ignored | array | Yes | Keys of comments that are not directives (nor tokens): not written, and not counted in changes. Empty when every key is a directive. |
message | string | Yes | One of: Magic comments updated successfully. |
{ "error": "directory must not be empty", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. Messages: “directory must not be empty”, “Invalid subdomain in Host header”, “Either directory or execId is required”, “comments object is required”, “directory is not a directory”, “Malformed query string” (Percent-encoding failed to decode. Use valid %XX escapes.), “Invalid JSON body” (Ensure the request body is valid JSON. Example: a backslash followed by a backtick is not a valid JSON escape.), “Request aborted before the body was fully received”. Other messages on this status are composed at runtime and are not listed here. |
details.code, when present, is one of: INVALID_PATH, NOT_A_DIRECTORY.
{ "error": "Directory is excluded from access", "code": "ERROR_403", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_403 | Forbidden | The service answers 403 with this code; the message names the cause. Messages: “Directory is excluded from access”, “Invalid directory: must be within scripts directory”. Other messages on this status are composed at runtime and are not listed here. |
{ "error": "Directory not found", "code": "ERROR_404", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_404 | Not found | The service answers 404 with this code; the message names the cause. |
{ "error": "Request body read timeout", "code": "ERROR_408", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_408 | Request timeout | The service answers 408 with this code; the message names the cause. |
{ "error": "Payload too large", "code": "ERROR_413", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_413 | Payload too large | The service answers 413 with this code; the message names the cause. |
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: “Too many distinct clients”, “Too many requests”. |
{ "error": "Internal server error", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
Scripts
Section titled “Scripts”Read, write, move, delete, list, and traverse scripts. Every path on this section follows the path conventions in the page introduction: the path a caller sends is instance-relative; paths returned in responses are scripts-root-relative with the scope prefix attached.
GET /api/v1/exec/scripts/list
Section titled “GET /api/v1/exec/scripts/list”List scripts and directories under a directory. Supports optional filters and recursion.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
dir | query | string | No | Directory to list, relative to the scripts directory. |
filter | query | string | No | Substring filter on file names. |
metadata | query | string | No | Metadata filter. |
label | query | string | No | Label filter. |
tags | query | string | No | Tag filter. |
mode | query | string | No | Mode filter. |
enabled | query | string | No | Enabled filter. |
websocket | query | string | No | WebSocket filter. |
remote | query | string | No | Keep only scripts by their remote operations: any (at least one of @remote-messages, @remote-call, @remote-eval is on), none (all off), or messages / call / eval (that one is on). Lists recursively and adds each script’s magicComments and remote summary, like the other filters. Any other value is refused with 400. |
recursive | query | string | No | Recurse into subdirectories. |
include_comments | query | string | No | Include magic comments in the response. |
exhaustive | query | boolean | No | When true, list every entry below dir recursively with its type (file, directory, symlink, fifo, socket, block-device, character-device, unknown), in entries instead of scripts. Nothing is hidden (dot-files, node_modules, .git, .hoody-cache, _sdk, _hoody) and no symlink is followed. Combine only with dir, execId and subdomain (other options are refused with 400). The answer is complete or an error, never partial: more than 10000 entries is 422 with details.code TOO_MANY_ENTRIES; a name or link target that is not valid UTF-8 is 422 with details.code NAME_NOT_UTF8 (details.parent, and the raw name as details.nameHex); an entry or directory that disappears or is replaced during the scan, or a directory whose dev, inode, mtime or ctime changed after it was read (checked once the scan is done), restarts it, and a tree still changing after 3 restarts is 409 with details.code TREE_CHANGED. This is best effort, for a tree nobody mutates concurrently: a change undone within one timestamp tick, or on a filesystem without fine timestamps, can go unseen. |
execId | query | string | No | Optional execution scope. When provided, relative paths resolve under default/{execId}/ unless subdomain is also set. |
exec_id | query | string | No | Alias for execId (snake_case). |
subdomain | query | string | No | Optional subdomain namespace used with execId for path resolution. |
curl -X GET "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/scripts/list?dir=&recursive=true" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.scripts.list({ recursive: 'true' });{ "directory": "default/1", "count": 4, "recursive": true, "filters": { "label": "", "tags": "", "mode": "", "enabled": "", "websocket": "" }, "scripts": [ { "name": "deploy.ts", "path": "default/1/deploy.ts", "isDirectory": false }, { "name": "jobs", "path": "default/1/jobs", "isDirectory": true } ]}| Field | Type | Required | Description |
|---|---|---|---|
directory | string | Yes | Scripts-root-relative directory that was listed. |
count | integer | Yes | Number of entries returned. |
recursive | boolean | Yes | Whether subdirectories were traversed. |
filters | object | Yes | Filters applied to the listing. |
filters.label | string | No | |
filters.tags | string | No | |
filters.mode | string | No | |
filters.enabled | string | No | |
filters.websocket | string | No | |
filters.remote | string | No | |
scripts | array | Yes | Entries. |
scripts[].name | string | Yes | |
scripts[].path | string | Yes | Scripts-root-relative path. |
scripts[].isDirectory | boolean | Yes |
The response may also be the simpler shape:
{ "directory": "default/1", "count": 4, "scripts": [ { "name": "deploy.ts" }, { "name": "jobs" } ]}| Field | Type | Required | Description |
|---|---|---|---|
directory | string | Yes | Scripts-root-relative directory that was listed. |
count | integer | Yes | Number of entries returned. |
scripts | array | Yes | Entries. |
When exhaustive=true is passed, the response uses entries and exhaustive: true instead of scripts and filters:
{ "directory": "default/1", "exhaustive": true, "count": 4, "entries": [ { "name": "deploy.ts", "path": "default/1/deploy.ts", "type": "file", "size": 92, "links": 1 }, { "name": "jobs", "path": "default/1/jobs", "type": "directory" } ]}| Field | Type | Required | Description |
|---|---|---|---|
directory | string | Yes | Scripts-root-relative directory that was listed. |
exhaustive | boolean | Yes | True when every entry below the directory is enumerated. |
count | integer | Yes | Number of entries returned. |
entries | array | Yes | Every entry below the directory, sorted by path. |
entries[].name | string | Yes | Entry name. |
entries[].path | string | Yes | Path relative to the scripts directory. |
entries[].type | string | Yes | Entry type from lstat. One of: file, directory, symlink, fifo, socket, block-device, character-device, unknown. A symlink is reported, never followed. |
entries[].size | integer | No | Size in bytes (files only). |
entries[].links | integer | No | Hard-link count (files only); above 1 means the same bytes have another name. |
entries[].target | string | No | Link target as stored (symlinks only). |
{ "error": "exhaustive must be true or false", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. Messages: “exhaustive must be true or false”, “exhaustive=true takes only dir, execId and subdomain: other options would hide entries”, “dir is not a directory”, “Malformed query string” (Percent-encoding failed to decode. Use valid %XX escapes.). Other messages on this status are composed at runtime and are not listed here. |
details.code, when present, is one of: NOT_A_DIRECTORY, INVALID_PATH.
{ "error": "Directory is excluded from script discovery", "code": "ERROR_403", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_403 | Forbidden | The service answers 403 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
{ "error": "Directory not found", "code": "ERROR_404", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_404 | Not found | The service answers 404 with this code; the message names the cause. |
The tree was still changing after 3 scan restarts, so the scan was abandoned.
{ "error": "Tree changed during scan", "code": "ERROR_409", "timestamp": "2026-01-01T00:00:00.000Z", "details": { "dir": "default/1", "code": "TREE_CHANGED", "attempts": 3 }}| Field | Type | Required | Description |
|---|---|---|---|
error | string | Yes | Human-readable error message. |
code | string | Yes | Machine-readable error code. |
timestamp | string | Yes | ISO 8601 timestamp emitted by the server. |
details | object | No | Optional additional error details. |
details.dir | string | No | Directory that was being scanned. |
details.code | string | No | The specific failure. One of: TREE_CHANGED. |
details.attempts | number | No | Number of scan attempts made before the server gave up. |
The scan could not finish because the tree is too large, or a name or link target is not valid UTF-8.
{ "error": "Too many entries", "code": "ERROR_422", "timestamp": "2026-01-01T00:00:00.000Z", "details": { "dir": "default/1", "code": "TOO_MANY_ENTRIES", "limit": 10000 }}| Field | Type | Required | Description |
|---|---|---|---|
error | string | Yes | Human-readable error message. |
code | string | Yes | Machine-readable error code. |
timestamp | string | Yes | ISO 8601 timestamp emitted by the server. |
details | object | No | Optional additional error details. |
details.dir | string | No | Directory that was being scanned. |
details.code | string | No | The specific failure. One of: TOO_MANY_ENTRIES, NAME_NOT_UTF8. |
details.limit | integer | No | Limit that was exceeded (when code is TOO_MANY_ENTRIES). |
details.parent | string | No | Parent directory of the offending entry. |
details.nameHex | string | No | Raw name as hex when not valid UTF-8. |
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: “Too many distinct clients”, “Too many requests”. |
{ "error": "Internal server error", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
GET /api/v1/exec/scripts/read
Section titled “GET /api/v1/exec/scripts/read”Read the contents of a script along with its magic comments and filesystem metadata.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
path | query | string | Yes | Script to read, instance-relative. |
execId | query | string | No | Optional execution scope. When provided, relative paths resolve under default/{execId}/ unless subdomain is also set. |
exec_id | query | string | No | Alias for execId (snake_case). |
subdomain | query | string | No | Optional subdomain namespace used with execId for path resolution. |
curl -X GET "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/scripts/read?path=deploy.ts" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.scripts.read({ path: 'deploy.ts' });{ "path": "deploy.ts", "resolvedPath": "default/1/deploy.ts", "content": "// @enabled true\n// @timeout 30s\nexport default async function() {\n return { ok: true };\n}\n", "magicComments": { "enabled": true, "timeout": "30s" }, "metadata": { "size": 92, "created": "2026-01-01T00:00:00.000Z", "modified": "2026-01-02T00:00:00.000Z", "isDirectory": false, "extension": ".ts" }}| Field | Type | Required | Description |
|---|---|---|---|
path | string | Yes | Path as supplied by the caller. |
resolvedPath | string | Yes | Scripts-root-relative path of the file that was read. |
content | string | Yes | File contents. |
magicComments | object | Yes | Parsed magic comments; null when none are present. |
metadata | object | Yes | Filesystem metadata. |
metadata.size | integer | Yes | File size in bytes. |
metadata.created | string | Yes | ISO 8601 creation timestamp. |
metadata.modified | string | Yes | ISO 8601 modification timestamp. |
metadata.isDirectory | boolean | Yes | Always false for this endpoint. |
metadata.extension | string | Yes | Lowercase file extension including the dot. |
{ "error": "path parameter is required", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. Messages: “path parameter is required”, “path is a directory; use scripts/list”, “Malformed query string” (Percent-encoding failed to decode. Use valid %XX escapes.). Other messages on this status are composed at runtime and are not listed here. |
details.code, when present, is one of: INVALID_PATH, IS_A_DIRECTORY.
{ "error": "Path is excluded from access", "code": "ERROR_403", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_403 | Forbidden | The service answers 403 with this code; the message names the cause. Messages: “Path is excluded from access”, “Invalid path: resolves through a symlink”. Other messages on this status are composed at runtime and are not listed here. |
{ "error": "Script not found", "code": "ERROR_404", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_404 | Not found | The service answers 404 with this code; the message names the cause. |
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: “Too many distinct clients”, “Too many requests”. |
{ "error": "Internal server error", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
POST /api/v1/exec/scripts/write
Section titled “POST /api/v1/exec/scripts/write”Create or replace a script. By default, parent directories are created and the script is validated before it is written.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
execId | query | string | No | Optional execution scope. When provided, relative paths resolve under default/{execId}/ unless subdomain is also set. |
exec_id | query | string | No | Alias for execId (snake_case). |
subdomain | query | string | No | Optional subdomain namespace used with execId for path resolution. |
Request Body
Section titled “Request Body”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
path | string | Yes | Path of the script to write. | |
content | string | Yes | Script contents. | |
createDirs | boolean | No | true | Create parent directories if they do not exist. |
validate | boolean | No | true | Validate the script before writing. |
ifNotExists | boolean | No | false | Create only. When true, do not replace an existing file. With the name already taken the answer is 409 and nothing is written; otherwise the new file is created as one atomic step. |
typecheck | boolean | No | false | When true, also type-check the script with the TypeScript compiler against the hoody-sdk declarations. A type error refuses the write with 400. |
execId | string | No | Optional execution scope in request body. Query execId/exec_id takes precedence when both are provided. | |
exec_id | string | No | Alias for execId (snake_case). | |
subdomain | string | No | Optional subdomain namespace used with execId for path resolution. |
curl -X POST "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/scripts/write" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "path": "jobs/clean.ts", "content": "// @enabled true\nexport default async function() { return { ok: true }; }\n" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.scripts.write({ path: 'jobs/clean.ts', content: '// @enabled true\nexport default async function() { return { ok: true }; }\n'});{ "path": "jobs/clean.ts", "resolvedPath": "default/1/jobs/clean.ts", "created": true, "updated": false, "size": 64, "modified": "2026-01-02T00:00:00.000Z", "validated": true, "env": null, "schedule": { "action": "registered", "reason": "cron expression parsed" }}| Field | Type | Required | Description |
|---|---|---|---|
path | string | Yes | Path as supplied by the caller. |
resolvedPath | string | Yes | Scripts-root-relative path of the file that was written. |
created | boolean | Yes | True when a new file was created. |
updated | boolean | Yes | True when an existing file was replaced. |
size | integer | Yes | File size in bytes. |
modified | string | Yes | ISO 8601 modification timestamp. |
validated | boolean | Yes | True when the script passed validation. |
env | object | No | Detected environment variables; present only when the script declares .env usage. |
schedule | object | No | Schedule registration result. |
schedule.action | string | Yes | |
schedule.reason | string | No |
{ "error": "path is required", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. Messages: “path is required”, “content is required”, “content must be a string”, “ifNotExists must be a boolean”, “Script path holds a character the router refuses, so no URL could reach it. Use A-Z a-z 0-9 - _ . / (a [param] segment is filled from the URL)”, “Script validation failed”, “Invalid JSON”, “Malformed query string” (Percent-encoding failed to decode. Use valid %XX escapes.), “Invalid JSON body” (Ensure the request body is valid JSON. Example: a backslash followed by a backtick is not a valid JSON escape.), “Request aborted before the body was fully received”. Other messages on this status are composed at runtime and are not listed here. |
details.code, when present, is one of: INVALID_PATH, UNROUTABLE_PATH, REDACTED_SECRET_UNRESOLVED, NOT_A_DIRECTORY, IS_A_DIRECTORY.
{ "error": "Path is excluded from access", "code": "ERROR_403", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_403 | Forbidden | The service answers 403 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
{ "error": "Parent directory not found. Set createDirs=true to create it", "code": "ERROR_404", "timestamp": "2026-01-01T00:00:00.000Z", "details": { "path": "api/users/list.ts", "code": "PARENT_NOT_FOUND" }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_404 | Not found | The service answers 404 with this code; the message names the cause. |
details.code, when present, is PARENT_NOT_FOUND: a parent directory of path does not exist and createDirs is false.
{ "error": "Request body read timeout", "code": "ERROR_408", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_408 | Request timeout | The service answers 408 with this code; the message names the cause. |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_409 | Conflict | The service answers 409 with this code; the message names the cause. | — |
{ "error": "Payload too large", "code": "ERROR_413", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_413 | Payload too large | The service answers 413 with this code; the message names the cause. |
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: “Too many distinct clients”, “Too many requests”. |
{ "error": "File created, but its temporary file could not be removed", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Messages: “File created, but its temporary file could not be removed”, “Internal server error”. Other messages on this status are composed at runtime and are not listed here. |
{ "error": "Filesystem unavailable", "code": "ERROR_503", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
The code field for 503 carries the value ERROR_503. The spec provides no further error-code table for this status. |
POST /api/v1/exec/scripts/move
Section titled “POST /api/v1/exec/scripts/move”Move a script to a new path. By default, the move fails with 409 when the destination already exists; pass overwrite: true to replace it. The 409 status is also returned when the source is missing under some conditions; the 404 status covers a missing source in the general case.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
execId | query | string | No | Optional execution scope. When provided, relative paths resolve under default/{execId}/ unless subdomain is also set. |
exec_id | query | string | No | Alias for execId (snake_case). |
subdomain | query | string | No | Optional subdomain namespace used with execId for path resolution. |
Request Body
Section titled “Request Body”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
from | string | Yes | Source path. | |
to | string | Yes | Destination path. | |
overwrite | boolean | No | false | Replace the destination if it already exists. |
execId | string | No | Optional execution scope in request body. | |
exec_id | string | No | Alias for execId (snake_case). | |
subdomain | string | No | Optional subdomain namespace used with execId for path resolution. |
curl -X POST "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/scripts/move" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "from": "deploy.ts", "to": "jobs/deploy.ts", "overwrite": false }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.scripts.move({ from: 'deploy.ts', to: 'jobs/deploy.ts', overwrite: false});{ "moved": true, "from": "deploy.ts", "to": "jobs/deploy.ts", "resolvedFrom": "default/1/deploy.ts", "resolvedTo": "default/1/jobs/deploy.ts", "size": 92}| Field | Type | Required | Description |
|---|---|---|---|
moved | boolean | Yes | True on success. |
from | string | Yes | Source path as supplied. |
to | string | Yes | Destination path as supplied. |
resolvedFrom | string | Yes | Scripts-root-relative source path. |
resolvedTo | string | Yes | Scripts-root-relative destination path. |
size | integer | Yes | Size in bytes of the moved file. |
{ "error": "from and to paths are required", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. Messages: “from and to paths are required”, “Destination holds a character the router refuses, so no URL could reach the script. Use A-Z a-z 0-9 - _ . / (a [param] segment is filled from the URL)”, “Malformed query string” (Percent-encoding failed to decode. Use valid %XX escapes.), “Invalid JSON body” (Ensure the request body is valid JSON. Example: a backslash followed by a backtick is not a valid JSON escape.), “Request aborted before the body was fully received”. Other messages on this status are composed at runtime and are not listed here. |
details.code, when present, is one of: INVALID_PATH, UNROUTABLE_PATH, NOT_A_DIRECTORY, IS_A_DIRECTORY, REDACTED_SECRET_UNRESOLVED.
{ "error": "Path is excluded from access", "code": "ERROR_403", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_403 | Forbidden | The service answers 403 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
{ "error": "Source script not found", "code": "ERROR_404", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
The code field for 404 carries the value ERROR_404. The spec provides no further error-code table for this status. |
{ "error": "Request body read timeout", "code": "ERROR_408", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_408 | Request timeout | The service answers 408 with this code; the message names the cause. |
{ "error": "Destination already exists", "code": "ERROR_409", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
The code field for 409 carries the value ERROR_409. The spec provides no further error-code table for this status. |
{ "error": "Payload too large", "code": "ERROR_413", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_413 | Payload too large | The service answers 413 with this code; the message names the cause. |
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: “Too many distinct clients”, “Too many requests”. |
{ "error": "Internal server error", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
{ "error": "Filesystem unavailable", "code": "ERROR_503", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
The code field for 503 carries the value ERROR_503. The spec provides no further error-code table for this status. |
DELETE /api/v1/exec/scripts/delete
Section titled “DELETE /api/v1/exec/scripts/delete”Delete a script. A safety confirmation is required.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
path | query | string | Yes | Script to delete. |
confirm | query | string | Yes | Safety confirmation. Must be the literal true. |
execId | query | string | No | Optional execution scope. |
exec_id | query | string | No | Alias for execId (snake_case). |
subdomain | query | string | No | Optional subdomain namespace used with execId for path resolution. |
curl -X DELETE "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/scripts/delete?path=jobs%2Fclean.ts&confirm=true" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.scripts.delete({ path: 'jobs/clean.ts', confirm: 'true' });{ "deleted": true, "path": "jobs/clean.ts", "resolvedPath": "default/1/jobs/clean.ts", "size": 64, "type": "file"}| Field | Type | Required | Description |
|---|---|---|---|
deleted | boolean | Yes | True on success. |
path | string | Yes | Path as supplied by the caller. |
resolvedPath | string | Yes | Scripts-root-relative path of the deleted file. |
size | integer | Yes | Size in bytes of the deleted file. |
type | string | Yes | The lstat type of what was removed. One of: file, symlink, fifo, socket, block-device, character-device, unknown. A symlink is removed itself (never followed; its target is untouched). |
target | string | No | The removed link’s target as stored (symlinks only). |
{ "error": "confirm=true parameter required for safety", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. Messages: “path parameter is required”, “confirm=true parameter required for safety”, “Malformed query string” (Percent-encoding failed to decode. Use valid %XX escapes.). Other messages on this status are composed at runtime and are not listed here. |
details.code, when present, is one of: INVALID_PATH, IS_A_DIRECTORY.
{ "error": "Path is excluded from access", "code": "ERROR_403", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_403 | Forbidden | The service answers 403 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
{ "error": "Script not found", "code": "ERROR_404", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
The code field for 404 carries the value ERROR_404. The spec provides no further error-code table for this status. |
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: “Too many distinct clients”, “Too many requests”. |
{ "error": "Internal server error", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
{ "error": "Filesystem unavailable", "code": "ERROR_503", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
The code field for 503 carries the value ERROR_503. The spec provides no further error-code table for this status. |
POST /api/v1/exec/scripts/tree
Section titled “POST /api/v1/exec/scripts/tree”Return a directory tree rooted at a base directory. Use includeMetadata to enrich each node with file metadata.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
execId | query | string | No | Optional execution scope. When provided, relative paths resolve under default/{execId}/ unless subdomain is also set. |
exec_id | query | string | No | Alias for execId (snake_case). |
subdomain | query | string | No | Optional subdomain namespace used with execId for path resolution. |
Request Body
Section titled “Request Body”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
baseDir | string | No | "" | Directory to use as the tree root, relative to the scripts directory. |
maxDepth | integer | No | 10 | Maximum depth to traverse. |
includeMetadata | boolean | No | false | Include file metadata on each node. |
execId | string | No | Optional execution scope in request body. | |
exec_id | string | No | Alias for execId (snake_case). | |
subdomain | string | No | Optional subdomain namespace used with execId for path resolution. |
curl -X POST "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/scripts/tree" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "baseDir": "", "maxDepth": 5, "includeMetadata": false }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.scripts.getTree({ baseDir: '', maxDepth: 5, includeMetadata: false});{ "baseDir": "default/1", "tree": [ { "name": "deploy.ts", "path": "default/1/deploy.ts", "type": "file" }, { "name": "jobs", "path": "default/1/jobs", "type": "directory", "children": [ { "name": "clean.ts", "path": "default/1/jobs/clean.ts", "type": "file" } ] } ]}| Field | Type | Required | Description |
|---|---|---|---|
baseDir | string | Yes | Scripts-root-relative directory used as the tree root. |
tree | array | Yes | Tree nodes. |
{ "error": "baseDir is not a directory", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. Messages: “baseDir is not a directory”, “Malformed query string” (Percent-encoding failed to decode. Use valid %XX escapes.), “Invalid JSON body” (Ensure the request body is valid JSON. Example: a backslash followed by a backtick is not a valid JSON escape.), “Request aborted before the body was fully received”. Other messages on this status are composed at runtime and are not listed here. |
details.code, when present, is one of: NOT_A_DIRECTORY, INVALID_PATH.
{ "error": "Directory is excluded from script discovery", "code": "ERROR_403", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_403 | Forbidden | The service answers 403 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
{ "error": "Directory not found", "code": "ERROR_404", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_404 | Not found | The service answers 404 with this code; the message names the cause. |
{ "error": "Request body read timeout", "code": "ERROR_408", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_408 | Request timeout | The service answers 408 with this code; the message names the cause. |
{ "error": "Payload too large", "code": "ERROR_413", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_413 | Payload too large | The service answers 413 with this code; the message names the cause. |
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: “Too many distinct clients”, “Too many requests”. |
{ "error": "Internal server error", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |