Daemon Management
Section titled “Daemon Management”The Hoody Container Daemon supervises long-running programs through supervisord. This page documents the endpoints for listing, inspecting, creating, editing, removing, and resetting supervised programs, plus launching and managing ephemeral (Quick Start) programs. Use these endpoints whenever you need to add a custom service to a container, change an existing program’s settings, run a one-off task, or reset to a clean default configuration.
All endpoints in this page are reached through the container’s daemon-1 hostname: https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com.
Programs
Section titled “Programs”GET /api/v1/daemon/programs
Section titled “GET /api/v1/daemon/programs”Lists every configured program. Combine the filters to narrow the result by hoody_kit, lazy_load, enabled, or boot status. Use the port filters to find the program that owns a specific port or that overlaps with a port range, and name for an exact match on a single program. Pass include_status=true to attach runtime status to each entry, or include_stats=true for CPU and memory stats.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
hoody_kit | query | string | No | Filter by hoody_kit status. Use "true" for Kit programs only, "false" for user programs. |
lazy_load | query | string | No | Filter by lazy_load status. Use "true" for lazy-loaded programs, "false" for the others. A non-lazy program starts on its own only when it is enabled and has boot: true. |
enabled | query | string | No | Filter by enabled status. Use "true" for enabled programs, "false" for disabled. |
boot | query | string | No | Filter by boot status. Use "true" for auto-start on boot, "false" for manual-start. |
name | query | string | No | Filter by exact program name, for example hoody-agent. The value is validated with the same rules a program name is validated with on write, so a malformed name returns HTTP 400 rather than an empty result. This is the only filter that selects one specific standard program: a program with no port_range cannot be matched by the port filters. |
port | query | integer | No | Filter by a single port, 1 to 65535. Returns only programs whose port_range includes this port. Example: ?port=8042. |
port_from | query | integer | No | Filter by port range start, 1 to 65535. Must be used with port_to. Returns programs whose port ranges overlap using program.start <= port_to AND program.end >= port_from. |
port_to | query | integer | No | Filter by port range end, 1 to 65535. Must be used with port_from. |
include_status | query | string | No | Include runtime status for each program. Adds a status field with the current state, instances, and process details. |
include_stats | query | string | No | Include CPU and memory stats. Turns status on only when include_status is omitted: an explicit include_status=false suppresses both. Adds a stats field with pid, started_at, cpu_percent, memory_rss_bytes, process_count, and a per-process breakdown. Only present for running programs. |
Response
Section titled “Response”hoody daemon programs list --include-status true -c "$CONTAINER_ID"curl -X GET "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/programs?include_status=true" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const { data: { programs } } = await client.daemon.programs.list({ include_status: 'true' });{ "programs": [ { "id": 1, "name": "web-server", "description": "Nginx web server", "enabled": true, "command": "nginx -g \"daemon off;\"", "boot": true, "delay_seconds": 5, "autorestart": "unexpected", "user": "www-data", "environment": { "NGINX_PORT": "80" }, "directory": "/var/www", "priority": 999, "stdout_logfile": "/hoody/storage/hoody-daemon/logs/web-server/stdout.log", "stderr_logfile": "/hoody/storage/hoody-daemon/logs/web-server/stderr.log", "lazy_load": false, "hoody_kit": false, "status": { "id": 1, "status": "running", "pid": 12345, "uptime": "2:15:30" } }, { "id": 7, "name": "hoody-notes", "enabled": true, "command": "node /hoody/plugins/hoody-notes/server.js", "boot": false, "delay_seconds": 0, "autorestart": "unexpected", "user": "root", "environment": {}, "directory": "/hoody/plugins/hoody-notes", "priority": 986, "port_range": { "start": 8042, "end": 8042 }, "port_param": "--port", "lazy_load": true, "hoody_kit": true, "status": { "type": "port-range", "running_instances": 0, "total_instances": 1, "instances": [] } } ]}{ "programs": []}A query parameter was present but unparseable (for example an invalid port, port_from, port_to, include_status, or include_stats). These are rejected rather than silently ignored: a dropped value widens the operation to its default.
GET /api/v1/daemon/programs/{id}
Section titled “GET /api/v1/daemon/programs/{id}”Retrieves detailed configuration for a single program by its numeric ID.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | Yes | Unique numeric identifier of the program. An id that is not a valid i32 (a word, an empty segment, a number too large) never reaches the handler: the path extractor refuses it with a plain-text 404, on every route that takes an id, which is NOT the documented JSON 404 of the routes that have one. An id that parses but names no program: on routes that document a 404 (this route does), returns the documented JSON 404 envelope ({success: false, error:...}); on the other routes, returns a 400 carrying the {success: false, error:...} envelope. |
Response
Section titled “Response”hoody daemon programs get 1 -c "$CONTAINER_ID"curl -X GET "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/programs/1" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const result = await client.daemon.programs.get(1);{ "success": true, "program": { "id": 1, "name": "web-server", "description": "Nginx web server", "enabled": true, "command": "nginx -g \"daemon off;\"", "boot": true, "delay_seconds": 5, "autorestart": "unexpected", "user": "www-data", "environment": { "NGINX_PORT": "80" }, "directory": "/var/www", "priority": 999, "stdout_logfile": "/hoody/storage/hoody-daemon/logs/web-server/stdout.log", "stderr_logfile": "/hoody/storage/hoody-daemon/logs/web-server/stderr.log", "lazy_load": false, "hoody_kit": false }}{ "success": false, "error": "Program with ID 999 not found"}GET /api/v1/daemon/programs/{id}/sandbox
Section titled “GET /api/v1/daemon/programs/{id}/sandbox”Reports what the daemon would enforce for this program’s stored sandbox policy and what the kernel is currently holding for it. effective is derived from the stored policy; live.table_present and live.rules are read back from nftables at request time and live.chain is derived from the stored revision, so the two disagreeing is itself the finding. A program with no effective sandbox answers 200 with rev: null and effective: null; configured carries the stored block or null. A policy that no longer validates or an unreadable state file does not fail the request: it is reported in problems with firewall: degraded.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | Yes | Unique numeric identifier of the program. An id that is not a valid i32 (a word, an empty segment, a number too large) never reaches the handler: the path extractor refuses it with a plain-text 404, on every route that takes an id, which is NOT the documented JSON 404 of the routes that have one. An id that parses but names no program: on routes that document a 404 (this route does), returns the documented JSON 404 envelope ({success: false, error:...}); on the other routes, returns a 400 carrying the {success: false, error:...} envelope. |
Response
Section titled “Response”hoody daemon programs sandbox get 1 -c "$CONTAINER_ID"curl -X GET "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/programs/1/sandbox" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const result = await client.daemon.programs.getSandbox(1);Effective sandbox, rules live:
{ "success": true, "configured": { "filesystem": { "read_only_root": true, "writable": ["/hoody/storage/apps/my-api"] }, "network": { "mode": "restricted", "bind_ports": [8000], "ingress_allow_from": ["203.0.113.0/24"], "ingress_rate_limit": "50/s" }, "process": { "max_memory": "512M", "max_pids": 256, "max_open_files": 1024 } }, "rev": "3f9c1a2b7e4d", "effective": { "bwrap_argv": ["/usr/bin/bwrap", "--die-with-parent", "--unshare-user", "--unshare-pid", "--unshare-ipc", "--cap-drop", "ALL", "--ro-bind", "/", "/", "--ro-bind", "/sys", "/sys", "--dev", "/dev", "--bind", "/proc", "/proc", "--tmpfs", "/tmp", "--tmpfs", "/var/tmp", "--bind-fd", "<per-launch-fd>", "/hoody/storage/apps/my-api", "--perms", "0000", "--tmpfs", "/root", "--chdir", "/hoody/storage/apps/my-api", "--", "/usr/local/bin/hoody-daemon", "exec-sandboxed-inner", "--", "<program argv>"], "scope_argv": ["/usr/bin/systemd-run", "--scope", "--quiet", "--collect", "--unit=hd-12-3f9c1a-<port>-<pid>", "-p", "MemoryMax=536870912", "-p", "MemorySwapMax=0", "-p", "OOMPolicy=continue", "-p", "TasksMax=256", "--", "/usr/local/bin/hoody-daemon", "exec-sandboxed-stage2"] }, "live": { "table_present": true, "chain": "hd_12_3f9c1a2b7e4d", "rules": [ { "rule": { "family": "ip", "table": "hoody_daemon", "chain": "hd_12_3f9c1a2b7e4d", "expr": [ {"match": {"op": "in", "left": {"ct": {"key": "state"}}, "right": ["established", "related"]}}, {"return": null} ] } } ] }, "firewall": "ok", "problems": []}Policy edited, the previous revision is still installed:
{ "success": true, "configured": { "network": { "mode": "restricted", "bind_ports": ["8080-8083"] } }, "rev": "5b2e0d9a1c4f", "effective": { "bwrap_argv": ["/usr/bin/bwrap", "--die-with-parent", "--unshare-user", "--unshare-pid", "--unshare-ipc", "--cap-drop", "ALL", "--ro-bind", "/", "/", "--", "<program argv>"], "scope_argv": ["/usr/bin/systemd-run", "--scope", "--quiet", "--collect", "--unit=hd-12-5b2e0d-<port>-<pid>", "-p", "TasksMax=64", "--", "/usr/local/bin/hoody-daemon", "exec-sandboxed-stage2"] }, "live": { "table_present": true, "chain": "hd_12_5b2e0d9a1c4f", "rules": [] }, "firewall": "draining", "problems": []}No effective sandbox:
{ "success": true, "configured": null, "rev": null, "effective": null, "live": { "table_present": false, "chain": null, "rules": [] }, "firewall": "none", "problems": []}{ "success": false, "error": "Program with ID 999 not found"}POST /api/v1/daemon/programs/add
Section titled “POST /api/v1/daemon/programs/add”Creates a new daemon program from a JSON request body. The program is validated and persisted to programs.json. Its supervisord definition is written at once only when it is enabled and has boot, lazy_load, a port_range or an effective sandbox; an enabled boot: true program is therefore applied during this call and can start at once. An ordinary program is registered when it is first started. A sandboxed program whose definition cannot be applied is a 400 AFTER it was persisted; an ordinary program’s apply failure does not fail the request.
For custom programs only (your own code/scripts), not system services.
Correct examples: node app.js, python my_script.py, ruby custom_server.rb, ./my-binary
Wrong examples: apache2, nginx, postgresql, mysql; use systemctl for these.
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
id | integer | No | Specific id to assign, 0 to 2147483646. Auto-assigned when omitted. |
name | string | Yes | Program name. Must be unique, start with a letter or digit, contain only letters, digits, ., - and _, up to 128 characters. |
description | string | No | Human-readable description. |
command | string | Yes | Command to execute with full arguments for your custom program. Must not contain a line feed, carriage return or NUL. |
user | string | Yes | System user (must exist on the system). A Linux account name matched case-insensitively: starts with a letter or _, continues with letters, digits, _ and -, may end in $, at most 32 bytes. A sandboxed program cannot run as an account resolving to uid 0. |
enabled | boolean | No | Enable the program immediately. Default: true. |
boot | boolean | No | Start automatically on system boot. Default: false. |
delay_seconds | integer | No | Seconds the program must STAY RUNNING for supervisord to consider the start successful (supervisord’s startsecs). The program is launched immediately. 0 does NOT disable the window: the daemon writes startsecs=1 at 0. Under autorestart "unexpected" or "true", a non-zero exit or a signal inside the window is a failed start, retried up to startretries times and then fatal. An end that is not a crash under autorestart (any end under "false", an exit 0 otherwise) is not a failed start, however quick: it is kept from supervisord until the window has passed, so a one-shot runs once. Under "false" and "unexpected" it is reported as exited as soon as it ends, with its exit code or signal in last_run; under "true" it is started again once the window has passed. A stop, edit or removal sent in that window can take up to this long to return. Hoody Kit programs keep supervisord’s own rule: any exit inside the window is a failed start, whatever autorestart says. Default: 0. |
autorestart | string | boolean | No | Restart policy: "unexpected" (default) restarts after an exit code other than 0, including a deliberate nonzero exit; "true" always; "false" never. Booleans true/false are accepted too and are stored and returned as sent. A program that exits on its own has finished, however quickly: any end under "false", and an exit 0 under "unexpected", is reported as exited with its result in last_run, so a one-shot runs once; under "true" an exit 0 is held until delay_seconds has passed and then started again. Under "unexpected" and "true" a non-zero exit or a signal inside the delay_seconds window is still a failed start, retried up to startretries times and then fatal. Hoody Kit programs keep supervisord’s time-based rule. Default: "unexpected". |
directory | string | No | Working directory: an absolute path of ASCII letters and digits, /, _, ., -, space and tab, with no .. path component, that must exist. Defaults to the user’s home directory, or /tmp. |
priority | integer | No | Start priority, lower starts first. Default: 999. Any u32 is accepted; no range is enforced. |
stdout_logfile | string | No | Path for standard output log. Must be under /hoody/storage/hoody-daemon/logs; any other path is a 400. Defaults to /hoody/storage/hoody-daemon/logs/<name>/stdout.log. |
stderr_logfile | string | No | Path for standard error log. Must be under /hoody/storage/hoody-daemon/logs; any other path is a 400. Defaults to /hoody/storage/hoody-daemon/logs/<name>/stderr.log. |
logs_enabled | boolean | No | Whether logging is enabled. Default: true. |
log_max_bytes | integer | No | Maximum size of each log file in bytes before rotation. Default: 5242880 (5MB). |
log_backups | integer | No | Number of rotated backup log files to keep. Default: 2. |
environment | object | No | Environment variables as key-value pairs. Names match ^[a-zA-Z_][a-zA-Z0-9_]*$ (up to 128 characters); values must not contain a line feed or carriage return. A value may not hold both a double and a single quote, or begin or end with either: supervisord’s environment line has no escape character, so such a value is refused with 400 naming the variable, unless the program has a terminal_id, no port_range and a user other than root. A value with a double quote and no single quote is written in single quotes, any other in double quotes. The stored map is not the whole process environment: with inject_container_env the program also receives PID 1’s environment (Kit programs only its HOODY_* variables in the default mode); without it, only the variables its command names. Explicit values override injected ones. |
inject_container_env | boolean | No | Whether the daemon adds the container’s environment (PID 1’s, read from /proc/1/environ) to the program’s. When absent: true for a Hoody Kit program (see hoody_kit), false for every other program. When true, the daemon’s injection mode applies: by default a Kit program gets only the HOODY_* variables and any other program all of them. When false, nothing from PID 1 is added, HOODY_* included, except the variables the program’s command names as $X, ${X} or %(ENV_X)s: each is resolved from the program’s environment, then PID 1, then (for $X and ${X} in a program without port_range) the daemon’s own environment, and only that variable is added. For a sandboxed program a named value that is not literal, or a loader name, is dropped. This controls the daemon’s own injection only: the program still inherits supervisord’s environment. |
port_range | object | No | Port range for multi-instance programs. {start, end}. Each port in the range creates a separate instance. Must not overlap any other stored program’s range. end - start + 1 must not exceed 4096. |
port_param | string | No | Parameter name for passing port (e.g. --port, -p). Valid ONLY together with port_range; sending it on its own is rejected with "port_param requires port_range to be set". Has no schema default on purpose. |
lazy_load | boolean | No | Enable lazy loading (autostart=false). When true, program/instances are NOT started automatically. Started on-demand by edge proxy. Default: false. Cannot be combined with boot: true. |
ready_port | integer | No | TCP readiness port, 1 to 65535. When set, a start with wait=true is reported successful only once supervisord reports the program running AND this port accepts a TCP connection. 0 is refused. |
display | string | integer | No | Hoody Display number for GUI programs, 1 to 40000 (display 0 is not accepted; only screen 0, so ":1" or ":1.0"). Accepts "1", ":1" or the number 1. |
terminal_id | integer | No | Hoody Terminal integration: session ID, 1 to 65535. Must be unique among persistent programs. Cannot be combined with an effective sandbox, or with port_range (a port-range program has no terminal). The program runs as its user, root or not, and gets TERM=xterm-256color unless its environment sets TERM. When the command ends, the program ends with the command’s exit code, so its status and autorestart follow the command, and clients attached to its terminal are disconnected with WebSocket close code 4404 (daemon program ended); a restarted program is reached by attaching again. See Watching and driving a terminal program. In interactive mode that is when the shell left after the command ends, and with tmux when the tmux client does. Output the program writes while hoody-terminal has no connection to it waits in the terminal, up to about 19 KB, and a program that writes more blocks until something attaches; once anything has attached or used the terminal’s REST automation, hoody-terminal stays connected until the program ends or the terminal session is deleted. A stop ends everything the program runs in its terminal: TERM first, KILL after 7 s for whatever ignores it. |
terminal_shell | string | No | Hoody Terminal integration: the shell that loads the environment, or tmux. A non-blank value requires terminal_id; the daemon accepts only an installed bash, zsh, fish, sh or tmux. With a shell, the command is one shell command line (pipes, && and variables work); without one, its words are passed as they are, with quotes grouping words and nothing expanded, and a # or ; after a space starts a comment, even inside quotes. Omit it, or send null, for direct execution. A blank string passes validation but is still used as a shell when terminal_id is set, which produces a broken command. |
terminal_interactive | boolean | No | Hoody Terminal integration: override auto-detection of interactive vs service mode. true forces interactive (the command runs and the shell stays), false forces service (the shell loads its rc file and is replaced by the command when it is a single command; a list, pipe or leading assignment runs as written), undefined/null auto-detects. |
webhooks | object | No | Webhook notification configuration. enabled and urls are REQUIRED whenever this object is present. At most 5 URLs when enabled is true. |
sandbox | object | No | Per-program confinement. On add, absent and null are the same thing. A policy rejection is a 400. Among its fields: network.udp (allow or deny; deny refuses UDP, UDP-Lite, ICMP ping, SCTP and AF_RXRPC sockets and io_uring in every network mode, and makes glibc resolve names over TCP), process.max_cpu ("<n>%", 1 to 102400, where 100% is one CPU), process.max_file_size (largest single file, 1K to 1024G) and process.tmp_size (size of each private /tmp and /var/tmp, 1M to 64G; requires private_tmp: true and bubblewrap 0.10.0 or newer, reported by /health as bwrap_tmpfs_size). Each of them makes the block effective. |
Response
Section titled “Response”hoody daemon programs create \ --name nodejs-app \ --description "Production Node.js application" \ --command "node server.js" \ --user nodejs \ --boot \ --delay-seconds 10 \ --autorestart unexpected \ --directory /opt/myapp \ --priority 100 \ --environment NODE_ENV=production --environment PORT=3000 \ --stdout-logfile /hoody/storage/hoody-daemon/logs/nodejs-app/stdout.log \ --stderr-logfile /hoody/storage/hoody-daemon/logs/nodejs-app/stderr.log \ -c "$CONTAINER_ID"curl -X POST "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/programs/add" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "name": "nodejs-app", "description": "Production Node.js application", "command": "node server.js", "user": "nodejs", "enabled": true, "boot": true, "delay_seconds": 10, "autorestart": "unexpected", "directory": "/opt/myapp", "priority": 100, "environment": { "NODE_ENV": "production", "PORT": "3000" }, "stdout_logfile": "/hoody/storage/hoody-daemon/logs/nodejs-app/stdout.log", "stderr_logfile": "/hoody/storage/hoody-daemon/logs/nodejs-app/stderr.log" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const result = await client.daemon.programs.create({ name: 'nodejs-app', description: 'Production Node.js application', command: 'node server.js', user: 'nodejs', enabled: true, boot: true, delay_seconds: 10, autorestart: 'unexpected', directory: '/opt/myapp', priority: 100, environment: { NODE_ENV: 'production', PORT: '3000' }, stdout_logfile: '/hoody/storage/hoody-daemon/logs/nodejs-app/stdout.log', stderr_logfile: '/hoody/storage/hoody-daemon/logs/nodejs-app/stderr.log'});The add response carries no top-level id; read it from program.id:
{ "success": true, "program": { "id": 2, "name": "nodejs-app", "description": "Production Node.js application", "enabled": true, "command": "node server.js", "boot": true, "delay_seconds": 10, "autorestart": "unexpected", "user": "nodejs", "environment": { "NODE_ENV": "production", "PORT": "3000" }, "directory": "/opt/myapp", "priority": 100, "stdout_logfile": "/hoody/storage/hoody-daemon/logs/nodejs-app/stdout.log", "stderr_logfile": "/hoody/storage/hoody-daemon/logs/nodejs-app/stderr.log", "lazy_load": false, "hoody_kit": false }}Validation failed or invalid configuration. The daemon returns its validator message verbatim in {"success": false, "error": "..."} with no wrapper.
{ "success": false, "error": "Program name cannot contain quotes"}{ "success": false, "error": "User nonexistent does not exist"}{ "success": false, "error": "sandbox.filesystem.writable: \"/hoody/storage\" overlaps a protected path"}{ "success": false, "error": "sandbox: user \"root\" resolves to uid 0, which cannot be sandboxed"}{ "success": false, "error": "Programs with lazy_load enabled cannot have boot enabled. Lazy-loaded programs are started on-demand by Hoody Proxy."}{ "success": false, "error": "port_param requires port_range to be set. Use port_range to define the port range for multi-instance programs."}The request body is larger than the daemon’s whole-body JSON limit of 1 MiB. Refused before the handler runs, with the {success: false, error} JSON envelope (not plain text). Separate from the sandbox policy’s own 64 KiB rendered-spec ceiling, which is a 400 from the validator.
{ "success": false, "error": "the request body is larger than the 1 MiB limit"}The program could not be persisted (the programs.json write failed; the error string starts with PERSIST: ), or, for a request that carries or replaces an effective sandbox block, the sandbox firewall state could not be read or applied (INTERNAL: ). For PERSIST, any firewall admission made for this request is reverted for this program alone; if that revert itself fails, the failure is logged and whatever rules were applied stay in place.
POST /api/v1/daemon/programs/edit/{id}
Section titled “POST /api/v1/daemon/programs/edit/{id}”Updates an existing program. The path id identifies it; an id in the body is ignored. A top-level field that is omitted or null keeps its stored value (except sandbox, where null clears the block), and the create defaults do NOT apply (changing user alone does not recompute directory). Three fields are replaced as a unit rather than merged: environment replaces the whole map ({} clears it), port_range needs both start and end, and a webhooks object must carry enabled and urls. A partial port_range or webhooks object is a 400 naming the missing field. webhooks: null does not clear stored webhooks. Unknown top-level keys are ignored, except a near-miss of sandbox, which is a 400.
When it takes effect. An edit that leaves the program disabled removes its supervisord definition at once. An edit to an enabled program with boot, lazy_load, a port_range, or a current or former effective sandbox is applied before the call returns and can restart the running instance; an apply failure is a 400 after saving only when a sandbox is or was involved.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | Yes | Unique numeric identifier of the program. An id that is not a valid i32 (a word, an empty segment, a number too large) never reaches the handler: the path extractor refuses it with a plain-text 404, on every route that takes an id, which is NOT the documented JSON 404 of the routes that have one. An id that parses but names no program is the handler’s own answer, a 400 carrying the {success: false, error:...} envelope. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Program name. Must be unique, start with a letter or digit, contain only letters, digits, ., - and _, up to 128 characters. |
description | string | No | Human-readable description. |
command | string | No | Command to execute with full arguments. Must not contain a line feed, carriage return or NUL. |
user | string | No | System user (must exist on the system). A Linux account name matched case-insensitively: starts with a letter or _, continues with letters, digits, _ and -, may end in $, at most 32 bytes. |
enabled | boolean | No | Enable the program immediately. |
boot | boolean | No | Start automatically on system boot. |
delay_seconds | integer | No | Seconds the program must STAY RUNNING for supervisord to consider the start successful (supervisord’s startsecs). The program is launched immediately. 0 does NOT disable the window: the daemon writes startsecs=1 at 0. Under autorestart "unexpected" or "true", a non-zero exit or a signal inside the window is a failed start, retried up to startretries times and then fatal. An end that is not a crash under autorestart (any end under "false", an exit 0 otherwise) is not a failed start, however quick: it is kept from supervisord until the window has passed, so a one-shot runs once. Under "false" and "unexpected" it is reported as exited as soon as it ends, with its exit code or signal in last_run; under "true" it is started again once the window has passed. A stop, edit or removal sent in that window can take up to this long to return. Hoody Kit programs keep supervisord’s own rule: any exit inside the window is a failed start, whatever autorestart says. |
autorestart | string | boolean | No | Restart policy: "unexpected", "true", "false", or booleans. A program that exits on its own under "false", or with exit 0 under "unexpected", has finished and is reported as exited with its result in last_run, however quickly it exits; a non-zero exit or a signal inside the delay_seconds window under "unexpected" or "true" is a failed start. Omitted or null, the stored policy is kept. |
directory | string | No | Working directory: an absolute path of ASCII letters and digits, /, _, ., -, space and tab, with no .. path component, that must exist. Omitted, the stored directory is kept, even when user changes. |
priority | integer | No | Start priority, lower starts first. Any u32 is accepted; no range is enforced. |
stdout_logfile | string | No | Path for standard output log. Must be under /hoody/storage/hoody-daemon/logs; any other path is a 400. Omitted or null, the stored path is kept. |
stderr_logfile | string | No | Path for standard error log. Must be under /hoody/storage/hoody-daemon/logs; any other path is a 400. Omitted or null, the stored path is kept. |
logs_enabled | boolean | No | Whether logging is enabled. Omitted, the stored value is kept. |
log_max_bytes | integer | No | Maximum size of each log file in bytes before rotation. Omitted, the stored value is kept. |
log_backups | integer | No | Number of rotated backup log files to keep. Omitted, the stored value is kept. |
environment | object | No | Environment variables as key-value pairs. On edit the map REPLACES the stored one: resend every variable you want kept; {} clears it. A value may not hold both a double and a single quote, or begin or end with either: supervisord’s environment line has no escape character, so such a value is refused with 400 naming the variable, unless the program has a terminal_id, no port_range and a user other than root. A value with a double quote and no single quote is written in single quotes, any other in double quotes. The stored map is not the whole process environment: with inject_container_env the program also receives PID 1’s environment (Kit programs only its HOODY_* variables in the default mode); without it, only the variables its command names. Explicit values override injected ones. |
inject_container_env | boolean | No | Whether the daemon adds the container’s environment (PID 1’s, read from /proc/1/environ) to the program’s. When absent: true for a Hoody Kit program (see hoody_kit), false for every other program. When true, the daemon’s injection mode applies: by default a Kit program gets only the HOODY_* variables and any other program all of them. When false, nothing from PID 1 is added, HOODY_* included, except the variables the program’s command names as $X, ${X} or %(ENV_X)s: each is resolved from the program’s environment, then PID 1, then (for $X and ${X} in a program without port_range) the daemon’s own environment, and only that variable is added. For a sandboxed program a named value that is not literal, or a loader name, is dropped. This controls the daemon’s own injection only: the program still inherits supervisord’s environment. On edit: absent leaves the stored value unchanged and there is no null; to return to the derived default, send the value it derives (true for a Hoody Kit program, false otherwise). |
port_range | object | No | Port range for multi-instance programs. Replaces the stored range; needs both start and end. Must not overlap any other stored program’s range. end - start + 1 must not exceed 4096. |
port_param | string | No | Parameter name for passing port. Valid ONLY together with port_range; sending it on its own is rejected with "port_param requires port_range to be set". |
lazy_load | boolean | No | Enable lazy loading (autostart=false). Cannot be combined with boot: true. |
ready_port | integer | No | TCP readiness port, 1 to 65535. When set, a start with wait=true succeeds only once supervisord reports running AND this port accepts a TCP connection. 0 is refused. |
display | string | integer | No | Hoody Display number for GUI programs, 1 to 40000 (display 0 is not accepted; only screen 0, so ":1" or ":1.0"). Accepts "1", ":1" or the number 1. |
terminal_id | integer | No | Hoody Terminal integration: session ID, 1 to 65535. Must be unique among persistent programs. Cannot be combined with an effective sandbox, or with port_range (a port-range program has no terminal). The program runs as its user, root or not, and gets TERM=xterm-256color unless its environment sets TERM. When the command ends, the program ends with the command’s exit code, so its status and autorestart follow the command, and clients attached to its terminal are disconnected with WebSocket close code 4404 (daemon program ended); a restarted program is reached by attaching again. See Watching and driving a terminal program. In interactive mode that is when the shell left after the command ends, and with tmux when the tmux client does. Output the program writes while hoody-terminal has no connection to it waits in the terminal, up to about 19 KB, and a program that writes more blocks until something attaches; once anything has attached or used the terminal’s REST automation, hoody-terminal stays connected until the program ends or the terminal session is deleted. A stop ends everything the program runs in its terminal: TERM first, KILL after 7 s for whatever ignores it. |
terminal_shell | string | No | Hoody Terminal integration: the shell that loads the environment, or tmux. A non-blank value requires terminal_id; the daemon accepts only an installed bash, zsh, fish, sh or tmux. With a shell, the command is one shell command line (pipes, && and variables work); without one, its words are passed as they are, with quotes grouping words and nothing expanded, and a # or ; after a space starts a comment, even inside quotes. Omit it, or send null, for direct execution. A blank string passes validation but is still used as a shell when terminal_id is set, which produces a broken command. Omitted or null on edit, the stored shell is kept. |
terminal_interactive | boolean | No | Hoody Terminal integration: override auto-detection of interactive vs service mode. |
webhooks | object | No | Webhook notification configuration. enabled and urls are REQUIRED whenever this object is present. On edit the two are replaced rather than merged. webhooks: null does not clear stored webhooks. |
sandbox | object | null | No | Per-program confinement. Three-state: absent leaves the current block unchanged, null clears it, and an object REPLACES it as a unit (no field-level merge). Accepts the same network.udp, process.max_cpu, process.max_file_size and process.tmp_size fields as on add. |
Response
Section titled “Response”hoody daemon programs update 1 \ --description "Updated description" \ --command "node server.js --production" \ --environment NODE_ENV=production --environment PORT=8080 \ -c "$CONTAINER_ID"curl -X POST "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/programs/edit/1" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "description": "Updated description", "command": "node server.js --production", "environment": { "NODE_ENV": "production", "PORT": "8080" } }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const result = await client.daemon.programs.update(1, { description: 'Updated description', command: 'node server.js --production', environment: { NODE_ENV: 'production', PORT: '8080' }});{ "success": true, "program": { "id": 1, "name": "web-server", "description": "Updated description", "enabled": true, "command": "node server.js --production", "boot": true, "delay_seconds": 5, "autorestart": "unexpected", "user": "www-data", "environment": { "NODE_ENV": "production", "PORT": "8080" }, "directory": "/var/www", "priority": 999, "stdout_logfile": "/hoody/storage/hoody-daemon/logs/web-server/stdout.log", "stderr_logfile": "/hoody/storage/hoody-daemon/logs/web-server/stderr.log", "lazy_load": false, "hoody_kit": false }}No program has that id, or the request was refused. An unknown id is a 400, not a 404. Also: validation failed. The daemon returns its message verbatim in {"success": false, "error": "..."}.
{ "success": false, "error": "Program with ID 999 not found"}{ "success": false, "error": "Programs with lazy_load enabled cannot have boot enabled. Lazy-loaded programs are started on-demand by Hoody Proxy."}The request body is larger than the daemon’s whole-body JSON limit of 1 MiB. Refused before the handler runs, with the {success: false, error} JSON envelope (not plain text). Separate from the sandbox policy’s own 64 KiB rendered-spec ceiling, which is a 400 from the validator.
{ "success": false, "error": "the request body is larger than the 1 MiB limit"}The program could not be persisted (the programs.json write failed; the error string starts with PERSIST: ), or, for a request that carries or replaces an effective sandbox block, the sandbox firewall state could not be read or applied (INTERNAL: ). For PERSIST, any firewall admission made for this request is reverted for this program alone.
POST /api/v1/daemon/programs/remove/{id}
Section titled “POST /api/v1/daemon/programs/remove/{id}”Permanently deletes a program from the configuration. The program row is deleted FIRST, and only then is its supervisord configuration withdrawn: what stops a running process is the reload that covers that withdrawal, not a stop issued beforehand. A withdrawal that cannot be confirmed answers 400 with the row already gone, says so, and leaves the configuration for the orphan sweep at the next daemon start. This is a destructive operation that cannot be undone.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | Yes | Unique numeric identifier of the program. An id that is not a valid i32 never reaches the handler: the path extractor refuses it with a plain-text 404. An id that parses but names no program is the handler’s own answer, a 400 carrying the {success: false, error:...} envelope. |
Response
Section titled “Response”hoody daemon programs delete 1 -c "$CONTAINER_ID"curl -X POST "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/programs/remove/1" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.daemon.programs.delete(1);{ "success": true, "id": 1}No program has that id, or the request was refused. An unknown id is a 400, not a 404.
{ "success": false, "error": "Program with ID 999 not found"}The program could not be persisted: the programs.json write failed and the error string starts with PERSIST: . A firewall-record retirement that fails does NOT produce a 500 either: the change completes, the failure is logged, the record stays with its rules and its port reservation, and the next daemon start reconciles it.
POST /api/v1/daemon/programs/reset
Section titled “POST /api/v1/daemon/programs/reset”Replaces the current programs.json with the initial default snapshot (programs.default.json) created at container setup time. Removes every managed program’s supervisord configuration, asks supervisord to reload, and applies the default boot programs. The reload is what stops the old processes, and its failure is logged rather than returned: a 200 means the defaults were persisted and applied, not that every earlier process is gone. Use this when programs have been misconfigured and a clean slate is needed.
Response
Section titled “Response”hoody daemon programs reset -c "$CONTAINER_ID"curl -X POST "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/programs/reset" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.daemon.programs.reset();{ "success": true}Reset failed (default file missing or corrupt). The default file lives at /hoody/storage/hoody-daemon/config/programs.default.json.
{ "success": false, "error": "Default programs file not found at '/hoody/storage/hoody-daemon/config/programs.default.json': No such file or directory. Cannot reset."}The defaults could not be persisted: the programs.json write failed and the error string starts with PERSIST: .
Lazy loading
Section titled “Lazy loading”lazy_load: true (boolean, default false) means the program does not start on its own, at boot or otherwise. It stays registered and costs nothing until something asks for it. The mechanism differs by program shape:
- For a program WITH
port_range,lazy_loadexplicitly rendersautostart=falseand every port in the range becomes its own registered-but-not-running instance. This is the form the edge proxy activates. - For a program WITHOUT
port_range, supervisord’sautostartsimply followsboot, and the API’s rejection oflazy_load: true+boot: trueis what forces it false. The standard renderer never readslazy_load.
A program with lazy_load: true and NO port_range is also not applied to supervisord on create at all; apply happens only when port_range is set or boot is true. Every lazy Kit service is registered with a single-port range ({start: p, end: p}) for exactly this reason.
Who starts it
Section titled “Who starts it”The Hoody Proxy starts lazy programs on the first request that routes to the program’s port. The proxy resolves the port from the hostname, then asks the daemon GET /api/v1/daemon/programs?lazy_load=true&port={port}&include_status=true. If a matching enabled program is not RUNNING, the proxy calls POST /api/v1/daemon/programs/{id}/start with { "if_not_running": true, "wait": true, "timeout": 30 } (plus "port" for a port-range program) and HOLDS the client request until the process reaches RUNNING, then proxies it. The caller sees one slow request, not an error.
Later requests hit a short per-port cache and pay nothing. The first request may wait while the program starts (up to about 30 seconds by default); later requests go straight through.
A first-request example
Section titled “A first-request example”- The user opens
https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com. - Nothing is listening yet, because
hoody-notesis registeredlazy_load: true. - The proxy queries
GET /api/v1/daemon/programs?lazy_load=true&port=8042&include_status=true, finds the program not running, and callsPOST /api/v1/daemon/programs/{id}/startwith{ "if_not_running": true, "wait": true, "timeout": 30, "port": 8042 }. - The request completes normally once the process is up, having been held for the startup.
The daemon lookup is purely by port (?lazy_load=true&port=N); it names no service, and the activation cache is keyed on containerIp:port. The hostname’s SERVICE LABEL decides only WHETHER the proxy runs the activation step, not how the daemon is queried. A http-<port> hostname does NOT trigger activation unless <port> is the port of an ENABLED SERVICE IN THE PROXY’S SERVICE REGISTRY (not merely a “Kit service”; the registry includes non-Kit entries such as daemon); the http- form otherwise yields no service name. The request still routes; it just reaches whatever is already listening. Start such an instance with POST /programs/{id}/start ({ "port": <port> } for a port-range program).
Shipped Kit programs
Section titled “Shipped Kit programs”The Kit programs registered lazy_load: true (13) are: hoody-browser, hoody-code, hoody-curl, hoody-display, hoody-exec, hoody-files, hoody-notes, hoody-notifications, hoody-pipe, hoody-run, hoody-sqlite, hoody-tunnel, hoody-workspaces.
Registered EAGERLY, NOT lazy (5): hoody-agent, hoody-cron, hoody-egress, hoody-watch, and hoody-terminal (where the terminal kit is enabled; it is registered with boot: true and a port_range but no lazy_load).
That is 18 Kit programs in total, 13 lazy. This is how most of the Kit behaves already: a container is not running fourteen daemons, it is running the ones someone has actually opened.
Quick Start
Section titled “Quick Start”POST /api/v1/daemon/quick-start
Section titled “POST /api/v1/daemon/quick-start”Creates and starts a temporary custom program. Custom programs only; system services (apache2, nginx, etc.) belong under systemctl.
Key features:
- Not saved to
programs.json(temporary only); tracked inephemeral.jsonwith its result, so it survives a daemon restart - Always created with
autostart=false(does not auto-start on reboot) - Lifetime: an optional
ttlbounds the whole execution from launch, across retries and restarts;expires_atis that deadline. When it passes the program is stopped andlast_run.errorreadsttl expired(checked every 10 seconds, so not exact). - Results: once the program finishes (
exitedwith no restart to follow,fatal, or stopped through the stop endpoint) its definition is withdrawn and its status,last_runand logs stay readable for 10 minutes; then status and logs answer 404. Log files are never deleted. - Daemon restart: a program that finished while the daemon was down keeps the exit code supervisord recorded; one still running is stopped and its
last_run.errorreadsdaemon restarted before the program finished. - The returned
temporary_idhas the formquick_<milliseconds>_<sequence>; it is not a numeric program id. Every response after creation carries it, errors included. wait: truereturns when the command finishes (200,status: "exited"andlast_run, whatever the exit code) or when a long-running program is running. A wait that times out does not cancel the launch. A start whose reply was lost answers 502 and is never retried.- Runs once by default:
autorestartdefaults to"false"here (persistent programs default to"unexpected"), so a failing command is not run again. Setautorestartto have it restarted.
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
command | string | Yes | Command to execute with full arguments. Must not contain a line feed, carriage return or NUL. |
user | string | Yes | System user to run as (must exist on the system). A Linux account name matched case-insensitively: starts with a letter or _, continues with letters, digits, _ and -, may end in $, at most 32 bytes. Quick-start programs cannot be sandboxed. |
name | string | No | Custom name (auto-generated if omitted). Starts with a letter or digit, contains only letters, digits, ., - and _, up to 128 characters. |
autorestart | string | boolean | No | Restart policy: "false" (default for a quick-start) never restarts; "unexpected" restarts after an exit code other than 0; "true" always; booleans are accepted too. Omitted, a quick-start runs once: any end, whatever its exit code or signal, is reported as exited with the code or signal in last_run (with wait: true, a 200), and a command that cannot be executed fails once and is not retried. A quick-start that should be restarted after a crash, such as a temporary server, sets "unexpected" or "true". Persistent programs keep "unexpected" as their default. A program that exits on its own under "false", or with exit 0 under "unexpected", has finished, however quickly it exits: it is reported as exited with its result in last_run and runs once. Under "unexpected" and "true" a non-zero exit or a signal inside the delay_seconds window is a failed start, retried up to startretries times and then fatal. |
directory | string | No | Working directory: an absolute path of ASCII letters and digits, /, _, ., -, space and tab, with no .. path component, that must exist. Defaults to the user’s home directory, or /tmp. |
environment | object | No | Environment variables as key-value pairs. A value may not hold both a double and a single quote, or begin or end with either: supervisord’s environment line has no escape character, so such a value is refused with 400 naming the variable, unless the program has a terminal_id, no port_range and a user other than root. A value with a double quote and no single quote is written in single quotes, any other in double quotes. The stored map is not the whole process environment: with inject_container_env: true the program also receives PID 1’s environment; without it, only the variables its command names. Explicit values override injected ones. |
inject_container_env | boolean | No | Whether the daemon adds the container’s environment (PID 1’s) to the program’s. Absent means false for a quick-start, whatever its directory. When true, the daemon’s injection mode applies (all variables by default). When false, nothing from PID 1 is added except the variables the command names as $X, ${X} or %(ENV_X)s, resolved from environment, then PID 1, then (for $X and ${X}) the daemon’s own environment. The program still inherits supervisord’s environment. |
priority | integer | No | Start priority, lower starts first. Default: 999. |
delay_seconds | integer | No | Seconds the program must STAY RUNNING for supervisord to consider the start successful. 0 does NOT disable the window: the daemon writes startsecs=1 at 0. A finished run that is not a crash under autorestart is kept from supervisord until the window has passed, so a one-shot runs once. |
stdout_logfile | string | No | Path for standard output log. Must be under /hoody/storage/hoody-daemon/logs; any other path is a 400. Defaults to /hoody/storage/hoody-daemon/logs/<name>/stdout.log. |
stderr_logfile | string | No | Path for standard error log. Must be under /hoody/storage/hoody-daemon/logs; any other path is a 400. Defaults to /hoody/storage/hoody-daemon/logs/<name>/stderr.log. |
logs_enabled | boolean | No | Whether logging is enabled. Default: true. |
log_max_bytes | integer | No | Maximum size of each log file in bytes before rotation. Default: 5242880 (5MB). |
log_backups | integer | No | Number of rotated backup log files to keep. Default: 2. |
ttl | integer | No | Time-to-live in seconds, counted from launch (when the daemon dispatches the start) and bounding the whole execution, across retries and restarts. The deadline is returned as expires_at. Expiry is checked every 10 seconds, so it is not an exact deadline; 0 expires at the first check. A very large value means no practical expiry. |
wait | boolean | No | Wait for the program to reach RUNNING state before returning. A command that finishes during the wait answers 200 with status: "exited" and its last_run, whatever the exit code. Default: false. |
timeout | integer | No | Seconds to wait for the program to be running when wait=true. Default: 30. The server clamps it to 300. |
display | string | integer | No | Hoody Display number for GUI programs, 1 to 40000 (display 0 is not accepted; only screen 0, so ":1" or ":1.0"). Accepts "1", ":1" or the number 1. |
terminal_id | integer | No | Hoody Terminal integration: session id, 0 to 65535. Quick-start does not check it for uniqueness. The program runs as its user, root or not, and gets TERM=xterm-256color unless its environment sets TERM. When the command ends, the program ends with the command’s exit code, so its status and autorestart follow the command, and clients attached to its terminal are disconnected with WebSocket close code 4404 (daemon program ended); a restarted program is reached by attaching again. See Watching and driving a terminal program. In interactive mode that is when the shell left after the command ends, and with tmux when the tmux client does. Output the program writes while hoody-terminal has no connection to it waits in the terminal, up to about 19 KB, and a program that writes more blocks until something attaches; once anything has attached or used the terminal’s REST automation, hoody-terminal stays connected until the program ends or the terminal session is deleted. A stop ends everything the program runs in its terminal: TERM first, KILL after 7 s for whatever ignores it. |
terminal_shell | string | No | Hoody Terminal integration: shell wrapper for environment loading. Quick-start does not validate the name or check that the shell is installed, and a shell without terminal_id is ignored. A blank string passes validation but is still used as a shell when terminal_id is set, which produces a broken command. Omit it, or send null, for direct execution. |
terminal_interactive | boolean | No | Hoody Terminal integration: override auto-detection. |
Response
Section titled “Response”QUICK_ID=$(hoody -o json daemon ephemeral programs start \ --command "node test-server.js" --user nodejs \ --name temp-test-server --directory /opt/test --ttl 1800 \ --environment PORT=9999 --environment NODE_ENV=test \ --wait -c "$CONTAINER_ID" | jq -r '.temporary_id')hoody -o json daemon ephemeral programs status "$QUICK_ID" -c "$CONTAINER_ID"curl -X POST "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/quick-start" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "command": "node test-server.js", "user": "nodejs", "name": "temp-test-server", "directory": "/opt/test", "ttl": 1800, "environment": { "PORT": "9999", "NODE_ENV": "test" }, "wait": true }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const result = await client.daemon.ephemeralPrograms.start({ command: 'node test-server.js', user: 'nodejs', name: 'temp-test-server', directory: '/opt/test', ttl: 1800, environment: { PORT: '9999', NODE_ENV: 'test' }, wait: true});For the request above (wait: true, ttl: 1800):
{ "success": true, "temporary_id": "quick_1731605789000_0", "name": "temp-test-server", "status": "running", "created_at": "2024-11-14T18:42:09Z", "expires_at": "2024-11-14T19:12:09Z"}expires_at is the deadline the daemon enforces: the launch time plus ttl.
Without wait the status is starting, and without ttl there is no expires_at:
{ "success": true, "temporary_id": "quick_1731605123000_0", "name": "temp-test-server", "status": "starting", "created_at": "2024-11-14T18:32:03Z"}With wait: true and the default autorestart ("false"), a command that finished during the wait answers exited with its last_run, whatever the exit code:
{ "success": true, "temporary_id": "quick_1791370000000_1", "status": "exited", "created_at": "2026-10-07T00:00:00+00:00", "last_run": { "exit_code": 0, "signal": null, "error": null, "ended_at": 1791370004210, "final": true }}Only success is guaranteed in the response. A success normally carries temporary_id, name, status and created_at, plus expires_at with a ttl and last_run once a run has ended. status is one of running, stopped, starting, stopping, backoff, exited, fatal, unknown or failed (its definition never loaded). supervisor_state appears only when supervisord’s own state differs from status.
Validation error, or a start that failed. The daemon returns its message verbatim in error, with success: false. Every error after the program was created carries its temporary_id, and last_run when a result is known (a start supervisord refused or gave up on, or a definition it did not load, whose status then reads failed). A validation error before creation carries only success and error.
{ "success": false, "error": "command is required"}{ "success": false, "error": "User invalid-user does not exist"}{ "success": false, "error": "Directory /nonexistent does not exist"}{ "success": false, "error": "Program name cannot contain quotes"}{ "success": false, "error": "sandbox is not supported for ephemeral programs"}The request body is larger than the daemon’s whole-body JSON limit of 1 MiB. Refused before the handler runs, with the {success: false, error} JSON envelope (not plain text). Separate from the sandbox policy’s own 64 KiB rendered-spec ceiling, which is a 400 from the validator.
{ "success": false, "error": "the request body is larger than the 1 MiB limit"}A record could not be written: the error starts with PERSIST: and the answer carries the temporary_id. Either the quick-start’s own record could not be written, and then nothing was started, or a later outcome could not be recorded, and then the answer carries that outcome in last_run. That outcome may be a finished run or a launch supervisord refused, with or without wait, so this error alone does not say whether the program ran.
{ "success": false, "temporary_id": "quick_1731605123456_0", "error": "PERSIST: cannot write ephemeral storage: No space left on device (os error 28)"}The start reached supervisord and its reply was lost: the error starts with INDETERMINATE: and carries the temporary_id. The program may be running, under its deadline. Nothing is retried; read its status before launching again.
{ "success": false, "temporary_id": "quick_1731605123456_0", "error": "INDETERMINATE: "}GET /api/v1/daemon/quick-start
Section titled “GET /api/v1/daemon/quick-start”Returns all currently tracked ephemeral programs with their current runtime status. An ephemeral program’s supervisord definition is withdrawn once it finishes, is stopped or expires; its entry (status, last_run) stays in this list for 10 minutes after that, and its log files are never deleted. Each entry may carry last_run (the result of its last ended run) and supervisor_state (only when supervisord’s own state differs from status); status can also read failed when the definition never loaded. expires_at is the absolute execution deadline, the launch time plus ttl.
This endpoint takes no parameters.
Response
Section titled “Response”hoody daemon ephemeral programs list -c "$CONTAINER_ID"curl -X GET "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/quick-start" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const { data: { ephemeral_programs } } = await client.daemon.ephemeralPrograms.list();{ "success": true, "count": 2, "ephemeral_programs": [ { "temporary_id": "quick_1731605123000_0", "name": "quick_python_1731605123456", "command": "python batch-job.py", "user": "worker", "status": "running", "pid": 12345, "uptime": "0:05:32", "created_at": "2024-11-14T18:32:03Z" }, { "temporary_id": "quick_1731605456000_0", "name": "my-temp-server", "command": "node server.js", "user": "nodejs", "status": "running", "pid": 12350, "uptime": "0:00:08", "created_at": "2024-11-14T18:37:36Z", "expires_at": "2024-11-14T19:37:36Z" } ]}{ "success": true, "count": 0, "ephemeral_programs": []}The request carried a query parameter. This route takes none, and an unknown parameter is refused with code INVALID_QUERY rather than ignored.
{ "success": false, "code": "INVALID_QUERY", "error": "Unknown query parameter 'fliter': this route takes no query parameters"}GET /api/v1/daemon/quick-start/{id}/status
Section titled “GET /api/v1/daemon/quick-start/{id}/status”Retrieves the current runtime status for a specific ephemeral program by its temporary_id. A finished program answers the status it finished with (exited, fatal, stopped, or failed when its definition never loaded) and its last_run for 10 minutes after it finished, then 404. The response has the same shape as the quick-start response: only success is guaranteed.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Temporary ID of the ephemeral program, as returned in temporary_id: quick_<unix-ms>_<sequence>. |
Response
Section titled “Response”hoody daemon ephemeral programs status "$QUICK_ID" -c "$CONTAINER_ID"curl -X GET "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/quick-start/quick_1731605123000_0/status" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const result = await client.daemon.ephemeralPrograms.getStatus('quick_1731605123000_0');Program is running:
{ "success": true, "temporary_id": "quick_1731605123000_0", "name": "quick_python_1731605123456", "status": "running", "pid": 12345, "uptime": "0:15:30", "created_at": "2024-11-14T18:32:03Z"}Program is stopped:
{ "success": true, "temporary_id": "quick_1731605456000_0", "name": "my-temp-server", "status": "stopped", "created_at": "2024-11-14T18:37:36Z"}Program with TTL expiry:
{ "success": true, "temporary_id": "quick_1731605789000_0", "name": "my-temp-server", "status": "running", "pid": 12360, "uptime": "0:01:10", "created_at": "2024-11-14T18:42:09Z", "expires_at": "2024-11-14T19:12:09Z"}Program finished, within the 10 minutes its result stays readable:
{ "success": true, "temporary_id": "quick_1731605123456_0", "name": "quick_python_1731605123456", "status": "exited", "created_at": "2024-11-14T18:32:03Z", "last_run": { "exit_code": 3, "signal": null, "error": null, "ended_at": 1731605183000, "final": true }}The ephemeral id is not a valid id, which is distinct from the 404 (a well-formed id that no ephemeral program has, or no longer has).
{ "success": false, "error": "Invalid ephemeral program ID format"}{ "success": false, "error": "Ephemeral program quick_1731605999999_0 not found"}GET /api/v1/daemon/quick-start/{id}/logs
Section titled “GET /api/v1/daemon/quick-start/{id}/logs”Retrieves the last N lines from an ephemeral program’s stdout or stderr log file. For a file over 10 MiB, reading starts 10 MiB before the end, so fewer lines can come back and the first can be partial. A log path outside the log root, and a missing, unreadable or non-UTF-8 file, are 400s even for an existing id; a file error carries type and log_file too. A malformed lines value is refused as plain text. Default ephemeral log paths are keyed on the program NAME: /hoody/storage/hoody-daemon/logs/<name>/stdout.log.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Ephemeral program temporary ID (temporary_id). |
type | query | string | No | Log stream: stdout or stderr. Default: stdout. |
lines | query | integer | No | Number of lines to return from the end of the file. Default 100; 0 returns empty content and a larger value is clamped to 10000 rather than refused. |
Response
Section titled “Response”hoody daemon ephemeral programs logs get "$QUICK_ID" --type stdout --lines 50 -c "$CONTAINER_ID"curl -X GET "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/quick-start/quick_1731605123000_0/logs?type=stdout&lines=50" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const result = await client.daemon.ephemeralPrograms.getLogs('quick_1731605123000_0', { type: 'stdout', lines: 50 });{ "success": true, "logs": "2024-11-14T18:32:03Z starting batch job\n2024-11-14T18:32:04Z processed 1024 records\n2024-11-14T18:32:05Z processed 2048 records", "type": "stdout", "lines": 3, "log_file": "/hoody/storage/hoody-daemon/logs/quick_python_1731605123456/stdout.log"}{ "success": false, "error": "Invalid log type. Must be 'stdout' or 'stderr'."}{ "success": false, "error": "Ephemeral program quick_1731605999999_0 not found"}POST /api/v1/daemon/quick-start/{id}/stop
Section titled “POST /api/v1/daemon/quick-start/{id}/stop”Stops the program, records its result and withdraws its supervisord configuration. The entry is KEPT: its status, last_run and logs stay readable for 10 minutes after it finished, and its log files are never deleted. To run the same command again, create a new ephemeral program.
The daemon asks supervisord to stop the program (a stop that is not confirmed is logged and does not end the request), records the result with status: "stopped" (the run’s own report when there is one, otherwise what supervisord recorded), then removes the group from supervisord and deletes its config file. cleaned_up: true means that withdrawal was CONFIRMED. cleaned_up: false means the result is recorded but the withdrawal is still owed; it is retried in the background and at the next daemon start before the entry is ever dropped. Either way the answer is 200. Once the withdrawal is confirmed the program cannot start again; while it is still owed and the stop itself was not confirmed, supervisord may still restart the program under its autorestart policy until a retry completes the withdrawal.
Stopping a program that already finished changes nothing about its result and answers 200; an id that is unknown, or whose retention has ended, is a 404.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Temporary ID of the ephemeral program to stop (temporary_id). |
Response
Section titled “Response”hoody daemon ephemeral programs stop "$QUICK_ID" -c "$CONTAINER_ID"curl -X POST "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/quick-start/quick_1731605123000_0/stop" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.daemon.ephemeralPrograms.stop('quick_1731605123000_0');{ "success": true, "temporary_id": "quick_1731605123000_0", "cleaned_up": true, "message": "Program stopped; its result and logs stay readable for 10 minutes"}The ephemeral id is not a valid id, which is distinct from the 404 (a well-formed id that no ephemeral program has, or no longer has).
{ "success": false, "error": "Invalid ephemeral program ID format"}{ "success": false, "error": "Ephemeral program quick_1731605999999_0 not found"}