Agent: Hooks
Section titled “Agent: Hooks”Manage agent lifecycle hooks. These endpoints let you list, upsert, delete, toggle, reload from disk, disable all, test-fire, and acknowledge hook-trust for a live session.
Lifecycle events
Section titled “Lifecycle events”The event field on every operation that accepts it must be one of the following values (the daemon rejects any value outside the enum):
NotificationPostToolUsePreCompactPreToolUseSessionEndSessionStartStopSubagentStartSubagentStopUserPromptSubmit
List and reload
Section titled “List and reload”GET /api/v1/agent/hooks
Section titled “GET /api/v1/agent/hooks”Lists the lifecycle hooks for a live session. The response is the verbatim daemon payload from hooks.list and includes the live + on-disk hook summaries, drift flags (stale), the per-scope settings files, and the trust probe. This is not a paginated list envelope.
session_id is required by the daemon RPC.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
session_id | query | string | No | Live session id (hooks are session-scoped; required by the daemon RPC). |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read resolves. |
X-Hoody-Realm | header | string | No | Per-request realm selector: “global” (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm. |
realm | query | string | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): “global” (not tied to a realm) or a 24-hex realm id. On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm. |
Response
Section titled “Response”{ "live": [ { "event": "PreToolUse", "matcher": "Bash", "command": "echo 'hook fired' >> /tmp/hooks.log", "name": "Log Bash commands", "description": "Append a line when a Bash command runs.", "timeout": 60, "disabled": false } ], "session": "67e89abc123def456789abcd890abcdef", "stale": [], "files": { "user": "/home/user/.hoody/settings.json", "project": "/home/user/project/.hoody/settings.local.json" }, "trust": { "hash": "f4c2e1a9b8d37091e3a4b2c5f8e0d1a9", "needs_ack": true, "high_risk": false }}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge (http.MaxBytesReader) before the handler reads it, so a well-formed-but-large body is a size violation, not a JSON syntax error. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "restriction_unknown", "message": "the login's realm restriction could not be read — retry"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.hooks.list({ session_id: '67e89abc123def456789abcd890abcdef' });curl -X GET 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/hooks?session_id=67e89abc123def456789abcd890abcdef' \ -H 'Authorization: Bearer <token>'POST /api/v1/agent/hooks/reload
Section titled “POST /api/v1/agent/hooks/reload”Reloads the hook configuration from disk (hooks.reload). Pass a live session_id to also receive the reloaded summary.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route. |
realm | query | string | No | In-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route. |
Request body
Section titled “Request body”The request body is optional. When supplied, all keys are passed through to the daemon and _-prefixed keys are ignored. Pass session_id to also receive the reloaded summary in the response: with it the session must be visible to the caller and the reply adds its summary; without it every live session reloads its own hooks and the reply has no summary.
{ "session_id": "67e89abc123def456789abcd890abcdef"}Response
Section titled “Response”{ "live": [ { "event": "PreToolUse", "matcher": "Bash", "command": "echo 'hook fired' >> /tmp/hooks.log", "name": "Log Bash commands", "description": "Append a line when a Bash command runs.", "timeout": 60, "disabled": false } ], "session": "67e89abc123def456789abcd890abcdef", "stale": [], "files": { "user": "/home/user/.hoody/settings.json", "project": "/home/user/project/.hoody/settings.local.json" }, "trust": { "hash": "f4c2e1a9b8d37091e3a4b2c5f8e0d1a9", "needs_ack": true, "high_risk": false }, "reloaded": true}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge before the handler reads it. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.hooks.reload({ session_id: '67e89abc123def456789abcd890abcdef' });curl -X POST 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/hooks/reload' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{"session_id":"67e89abc123def456789abcd890abcdef"}'Tool-call rules
Section titled “Tool-call rules”The agent’s tool-call rules live in the realm’s settings scopes. Use hooks.getRules to read them without a session, and hooks.setRules to replace them in one scope. Both hooks.setRules and the hook upsert/delete/toggle/disable-all paths require a hooks.createWriteIntent nonce.
GET /api/v1/agent/hooks/rules
Section titled “GET /api/v1/agent/hooks/rules”Returns the tool-call rules of the active realm’s settings with no session: the same rules block hooks.list returns, without its session half (live, timeout_ms and scopes; no session, stale, active or note, which compare against a running session). Use it where no session is live, such as a rules panel whose home session was reaped; to see what one live session checks and tells an agent, use sessions.listApplicableRules.
The user scope (the realm’s profile) is always read. X-Hoody-Cwd adds the project and local scopes of that directory; without it those scopes are not read or listed. X-Hoody-Config-Dir reads the override’s project and local scopes, and there is no user scope. Both must be absolute paths: a relative one is 400 bad_request, whichever header carries it. A settings file whose real location (symlinks resolved) is another realm’s profile, or the Global realm’s for a scoped realm, is never read, and no symlink is followed when it is opened. When it is the user file or under X-Hoody-Config-Dir, the whole read is 404 not_found. When it is the project or local file of X-Hoody-Cwd (a scoped realm’s cwd at or above the home directory, or a project .hoody linked into a profile), that layer is left out with a message in warnings and no revision, and a session started with that cwd leaves it out too. A file that cannot be read (not a regular file, over 4 MiB, unreadable) or parsed is left out with a message in warnings, like a skipped layer or a dropped rule.
live merges the scopes it read in load order, as a session started with the same cwd loads them. Each scopes[].revision is the value hooks.setRules’ expected_revision is checked against, for a write through a session in the same realm; the rules and the revisions come from one read of each file, so they always describe the same state. While an account switch is in flight during the read, it is refused with 503 service_unavailable rather than answered for the outgoing account; retry shortly. A box whose account identity is never resolved (an environment token only) is answered. A gateway pinned to a realm the signed-in account may not use gets 404 not_found, as the other realm-scoped reads do.
The block also lists the user scope’s per-container exec trust grants (exec_trust; revoke one with hooks.setRules). The rules change token (changes.get) moves when these files change, with or without a live session. Writes still need a live session: hooks.createWriteIntent, then hooks.setRules. Cache-Control: no-store.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read resolves. |
X-Hoody-Realm | header | string | No | Per-request realm selector: “global” (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm. |
realm | query | string | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): “global” (not tied to a realm) or a 24-hex realm id. On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm. |
Response
Section titled “Response”{ "rules": { "live": [ { "id": "limit-no-rm-rf", "text": "Never run rm -rf outside the workspace.", "kind": "limit", "applies_as": "checked", "tools": ["bash"], "agents": ["general"], "source": "/home/user/project/.hoody/settings.local.json" } ], "timeout_ms": 1200, "scopes": [ { "scope": "user", "revision": "01HABC9a4b2c5f8e0d1a9b8d37091e3a" }, { "scope": "project", "revision": "01HPRO9a4b2c5f8e0d1a9b8d37091e3a" }, { "scope": "local", "revision": "01HLOC9a4b1c5f8e0d1a9b8d37091e3a" } ], "exec_trust": [ { "container_id": "67e89abc123def456789abcd890abcdef", "realm": "_global", "name": "workspace-runner", "granted_at": "2024-09-14T12:00:00Z", "source": "rules_panel" } ], "warnings": [], "rule_warnings": [], "rules_complete": true }}| Field | Type | Description |
|---|---|---|
rules | object | The rules block of client.agent.hooks.list() without its session half. |
rules.live | array | The rules as they load from the scopes’ files now, merged in load order (user, project, local), checked and prompt kinds together. |
rules.live[].id | string | The rule id. |
rules.live[].text | string | The rule in plain words. |
rules.live[].kind | string | limit (a call that breaks the rule is denied), check_in (a call the rule covers asks the user first), guidance (standing advice the covered agents follow), or playbook (a procedure the covered agents follow when the situation its text describes comes up). limit and check_in are checked by Jev before covered tool calls; guidance and playbook are never checked, they are added to the system prompt of every session and helper they cover. |
rules.live[].applies_as | string | Output only, derived from kind: how the rule reaches the agent: checked (Jev checks covered tool calls against it; limit, check_in) or prompt (its text is in the covered agents’ system prompt; guidance, playbook). |
rules.live[].tools | array | The tools the rule is limited to; empty means every tool that is not read-only, except ask_question_to_user and report_outcome, which only a rule naming them covers. Always empty for guidance and playbook rules. |
rules.live[].agents | array | The agents whose calls the rule covers; empty means every agent. |
rules.live[].source | string | The settings file that declared the rule. |
rules.timeout_ms | integer | The effective Jev budget for one rules check in ms (the scopes’ rules_timeout_ms, else the default 3000). |
rules.scopes | array | One entry per scope read whose file could be read, in load order. project and local are listed only with X-Hoody-Cwd or X-Hoody-Config-Dir; user is not listed under X-Hoody-Config-Dir. |
rules.scopes[].scope | string | The settings scope: user, project, or local. |
rules.scopes[].revision | string | The scope file’s rules revision: pass it as hooks.setRules’ expected_revision. |
rules.exec_trust | array | The per-container exec trust grants of the user scope (the realm’s profile, or the X-Hoody-Config-Dir override): on each container, in its realm, hoody_exec_* calls ask no card, human-only actions included, until the row is removed with hooks.setRules. Empty when there are none or the user scope was not read. A row that fails validation is left out. |
rules.exec_trust[].container_id | string | The container id (24 hex) the grant is keyed by; a container name is never used. |
rules.exec_trust[].realm | string | The realm the grant applies in: _global or a realm id (24 hex). |
rules.exec_trust[].name | string | The container name when the grant was made, for display (at most 128 characters). |
rules.exec_trust[].granted_at | string | When the grant was made (RFC3339). |
rules.exec_trust[].source | string | card (an exec card answered with trust_container) or rules_panel (hooks.setRules, the default for a row sent without one). |
rules.warnings | array | Every problem the scopes’ files raised: a file that could not be read or parsed, a file skipped for a missing or wrong version, a rule that failed validation or is past its kind’s cap, and problems that leave every rule in place (an unknown hook event, a matcher that did not compile, an out-of-range rules_timeout_ms). The live.warnings of client.agent.hooks.list() carries the same messages for a session. Empty when there were none. |
rules.rule_warnings | array | The warnings that can leave a rule out: a scope file this read cannot show or that could not be read or parsed, a file whose rules were skipped for a missing or wrong version, a rule that failed validation or is past its kind’s cap. A hook-only warning is never listed here. Empty when every rule was read. |
rules.rules_complete | boolean | true when rule_warnings is empty: live holds every rule the scopes’ files declare. A client that must not act on a partial rule set checks this field, not warnings. |
{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "the rules could not be read while the account was changing; try again shortly"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Rules not readable right now | An account switch was in flight during the read, so no rules were returned. | Retry shortly. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.hooks.getRules();curl -X GET 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/hooks/rules' \ -H 'Authorization: Bearer <token>'Guarded writes
Section titled “Guarded writes”Every guarded write follows the same pattern:
- Call
hooks.createWriteIntentwithsession_id, the targetop, andscope. Receive a one-shotnoncethat binds to that tuple. The supportedopvalues areupsert,delete,toggle,set_disabled, andrules_set(used byhooks.setRules). - Call the matching write (
upsert,delete,toggle,set_disabled, orrules_set) withsession_id,scope, and the returnednonce. The RPC fails closed without the nonce.
POST /api/v1/agent/hooks/begin-write
Section titled “POST /api/v1/agent/hooks/begin-write”Issues the two-step write nonce required before every guarded hook mutation (upsert, delete, toggle, set_disabled, rules_set). The nonce is single-use and binds to the session_id + op + scope tuple; a nonce minted for op:toggle is not accepted by op:upsert, and so on.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route. |
realm | query | string | No | In-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route. |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id (hooks are session-scoped). |
op | string | Yes | The write the nonce authorizes: upsert (hooks.upsert), delete (hooks.delete), toggle (hooks.enable / hooks.disable), set_disabled (hooks.enableAll / hooks.disableAll) or rules_set (hooks.setRules). The nonce is rejected by any other op. |
scope | string | Yes | Scope of the settings file the write targets (for example project/user); the nonce binds to its resolved path. |
{ "session_id": "67e89abc123def456789abcd890abcdef", "op": "upsert", "scope": "project"}Response
Section titled “Response”{ "nonce": "01HDEF9a4b2c5f8e0d1a9b8d37091e3a", "op": "upsert", "scope": "project", "session_id": "67e89abc123def456789abcd890abcdef", "expires_at": "2024-09-14T12:00:00Z"}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge before the handler reads it. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.hooks.createWriteIntent({ session_id: '67e89abc123def456789abcd890abcdef', op: 'upsert', scope: 'project',});curl -X POST 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/hooks/begin-write' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{"session_id":"67e89abc123def456789abcd890abcdef","op":"upsert","scope":"project"}'POST /api/v1/agent/hooks/rules
Section titled “POST /api/v1/agent/hooks/rules”Replaces the tool-call rules in one settings scope. Rules are sentences checked by Jev before every tool call that is not read-only: a limit rule the call breaks denies it, and a check_in rule the call falls under asks the user first. ask_question_to_user and report_outcome are checked only by a rule whose tools list names them. If Jev cannot answer in time, the call is not let through: the main session asks, and a helper agent is denied.
This is a guarded WRITE: it requires the begin-write nonce minted by hooks.createWriteIntent for op:rules_set plus a live session_id. rules is the COMPLETE list for the scope and is validated all-or-nothing; send an empty array to remove every rule. The same write also carries the per-container exec trust grants (exec_trust, user scope only): the Rules panel adds and revokes them here, and with exec_trust sent rules may be omitted.
Rules are part of the hook trust hash, so on a fleet box the new set holds every covered call until it is acknowledged: read hooks.list’ trust.hash, then call hooks.trust. The reply carries the session’s rules and effective timeout after the reload, and the scope’s new revision.
Coverage: rules check the session’s own tool calls, helper agents, workflow steps (including inline $(bash:…) and execute_if shells), tool_orchestrator inner calls, a trusted skill’s !`cmd` shell commands, loop stop_when probes (only rules whose tools list names the probed tool, since every probe tool is read-only), a fusion member’s read-only exploration calls (only rules whose tools list names the tool; a check-in there is denied, since no one can be asked) and client.agent.tools.run() (POST /api/v1/agent/tools/{name}/run). Only these routes are checked: a tool run through any other entry point is not. A rule is matched by tool NAME, so a rule scoped to bash does not cover tools that run code by other means (run_codex, hoody_exec_run and similar); leave tools empty, or list those tools too.
An oversized input fails closed, except a file-writing tool’s (write_file, edit_file and their minified forms), which Jev sees as the path, the size and the first and last 8 KiB of each long text field. Auto-reply never approves a rules check-in: it is denied.
Known limits: a workflow’s own env values are not shown to Jev (only its input and step values are), so a command such as eval "$FOO" hides what it runs; and a confirmed run (confirm:true, which client.agent.tools.run() accepts only with the parked run’s confirm_token) passes a check-in, never a limit.
Guidance and playbook rules share the list and the revision, but Jev never checks them: each covered session and helper gets them in a delimited team-rules block of its system prompt (guidance first, then playbooks, each in settings order), at most 8192 bytes of rule lines per agent; sessions.listApplicableRules names what did not fit. A project-scope guidance or playbook rule comes from the repository’s own project settings file, so a repository can add text to the prompt. A scope holds at most 32 checked rules and 32 guidance and playbook rules, capped separately; across the loaded settings files each kind keeps its first 32 and the rest is dropped with a warning, so guidance and playbook rules never displace a checked rule.
A guidance or playbook rule’s text must not spell a <system-reminder> tag (matched case-insensitively on a compatibility-folded form, with spaces, underscores or dashes (a minus sign included) between the words, and an HTML-entity bracket with or without its semicolon or a \u003c / \x3c escape) or contain an invisible format character (refused 400 bad_request; dropped with a warning at load). A session sees an edit from its next model request (its next turn at the latest); a helper keeps the rules it was spawned with. prompt_blocks team_rules: false drops the block. They are not in the trust hash: adding one never holds checked calls pending an ack, and their text reaches the model on a fleet box without an ack (like the user’s agent instructions). Each rule in the reply and in hooks.list carries applies_as (checked | prompt), derived from kind; an applies_as sent here is ignored.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route. |
realm | query | string | No | In-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route. |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id (hooks are session-scoped). |
nonce | string | Yes | The single-use write nonce from hooks.createWriteIntent minted for op:rules_set plus this scope; the RPC fails closed without it. |
scope | string | No | Scope of the settings file to write (must match the nonce’s scope). |
rules | array | No | The complete rule list for the scope (at most 32 checked and 32 guidance/playbook rules; ids unique). An empty array removes the scope’s rules. |
rules[].id | string | Yes | Stable rule id: 1-64 characters of a-z, 0-9, _ or -, starting with a letter or digit. |
rules[].text | string | Yes | The rule in plain words, at most 500 characters, no control characters. |
rules[].kind | string | Yes | limit (a call that breaks the rule is denied), check_in (a call the rule covers asks the user first), guidance (standing advice the covered agents follow), or playbook (a procedure the covered agents follow when the situation its text describes comes up). limit and check_in are checked by Jev before covered tool calls; guidance and playbook are never checked, they are added to the system prompt of every session and helper they cover. A playbook’s text states the situation and the steps. |
rules[].tools | array | No | Optional: only check calls to these tools (hoody tool names or Claude Code aliases such as Bash; at most 32). Omit to check every tool that is not read-only (ask_question_to_user and report_outcome are checked only by a rule that names them). limit and check_in only: a guidance or playbook rule with tools is refused with 400 rule_tools_not_allowed. |
rules[].agents | array | No | Optional: only check calls made by these agents (agent names such as general or a custom agent’s name; at most 16, no duplicates). The acting agent is the session’s own agent for its calls (orchestrator in orchestrator mode), the helper’s agent for a spawned helper, and the step’s agent for a workflow agent step; a fusion member acts for the agent whose turn it serves. A native workflow step (bash, tool, inline shell) has no agent, so only a rule without agents covers it. Omit to check calls by every agent. |
timeout_ms | integer | No | Optional: the scope’s Jev budget for one rules check, 100 to 120000 ms; 0 removes it (back to the default 3000). Omit to leave it unchanged. This is the only way to write rules_timeout_ms. |
exec_trust | array | No | Optional: the COMPLETE list of per-container exec trust grants, replacing the scope’s (at most 64 rows, each (container_id, realm) once, validated all-or-nothing); [] revokes every grant. Accepted only for the user scope (the realm’s profile, or the X-Hoody-Config-Dir override); any other scope is 400. When it is sent, rules may be omitted and the rules stay as they are. The grants share the scope’s revision with the rules. |
exec_trust[].container_id | string | Yes | The container id (24 hex) the grant is keyed by; a container name is never used. |
exec_trust[].realm | string | Yes | The realm the grant applies in: _global or a realm id (24 hex). |
exec_trust[].name | string | No | The container name when the grant was made, for display (at most 128 characters). |
exec_trust[].granted_at | string | No | When the grant was made (RFC3339). |
exec_trust[].source | string | No | card (an exec card answered with trust_container) or rules_panel (hooks.setRules, the default for a row sent without one). |
expected_revision | string | No | Optional: the scope’s rules revision this edit is based on, from hooks.list rules.scopes[].revision. When it no longer matches the scope file (another tab, a hand edit, a timeout change), the write is refused with 409 revision_conflict, nothing is written, and details.current_revision carries the current value: re-read, re-apply the change, begin a new write (the refused call used up its nonce) and retry. The check and the write are one settings transaction. Omit it (or send null) to replace the rules unconditionally. The reply carries the new revision. The revision is bound to the realm of the session it is read and written through, so a revision read through another realm’s session is refused 409 revision_conflict, on a gateway with or without a realm pin. |
{ "session_id": "67e89abc123def456789abcd890abcdef", "nonce": "01HDEF9a4b2c5f8e0d1a9b8d37091e3a", "scope": "project", "rules": [ { "id": "limit-no-rm-rf", "text": "Never run rm -rf outside the workspace.", "kind": "limit", "tools": ["bash"], "agents": ["general"] } ], "timeout_ms": 1500, "expected_revision": "01HPRO9a4b2c5f8e0d1a9b8d37091e3a"}Response
Section titled “Response”{ "ok": true, "scope": "project", "revision": "01HPRO9b3c4d5e6f7a8b9c0d1e2f3a4b", "rules": { "live": [ { "id": "limit-no-rm-rf", "text": "Never run rm -rf outside the workspace.", "kind": "limit", "applies_as": "checked", "tools": ["bash"], "agents": ["general"], "source": "/home/user/project/.hoody/settings.local.json" } ], "timeout_ms": 1500 }}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
rule_tools_not_allowed | Rule kind takes no tools | A guidance or playbook rule named tools. Those kinds are not checked per tool call (they are prompt text), so a tools list would mean nothing; nothing was written. | Remove tools from the rule, or make it a limit or check_in rule. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "revision_conflict", "message": "the scope's rules changed since expected_revision was read; nothing was written"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
revision_conflict | Rules revision conflict | The scope’s rules (or rules_timeout_ms) changed since expected_revision was read, so nothing was written. details.current_revision carries the scope’s current revision. | Re-read client.agent.hooks.list() (rules.live and rules.scopes), re-apply the change, begin a new write and retry with the current revision. |
write_intent_invalid | Write intent invalid | The begin-write nonce was missing, expired, already spent, or minted for another op, session or scope. The nonce is consumed by the attempt, so a retry always needs a fresh one. Nothing was written. | Mint a new nonce with client.agent.hooks.createWriteIntent() for this exact op and scope, then retry. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge before the handler reads it. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
store_failed | Agent store failed | One of the agent’s own stores could not be read, parsed, locked or written, so the operation did not complete. The message names the store (for example “the agent’s settings file is damaged and was left unchanged”). A damaged store is left as it was rather than rewritten. | Retry once; if it persists, repair the named store inside the container. The agent’s log (GET /logs) records a “store failure” entry with the store, the failure kind (io, corrupt or lock) and a path-free cause such as “open: permission denied” or “invalid JSON at offset 12”; it does not name the file’s path. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.hooks.setRules({ session_id: '67e89abc123def456789abcd890abcdef', nonce: '01HDEF9a4b2c5f8e0d1a9b8d37091e3a', scope: 'project', rules: [ { id: 'limit-no-rm-rf', text: 'Never run rm -rf outside the workspace.', kind: 'limit', tools: ['bash'], agents: ['general'], }, ], timeout_ms: 1500, expected_revision: '01HPRO9a4b2c5f8e0d1a9b8d37091e3a',});curl -X POST 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/hooks/rules' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "session_id":"67e89abc123def456789abcd890abcdef", "nonce":"01HDEF9a4b2c5f8e0d1a9b8d37091e3a", "scope":"project", "rules":[ { "id":"limit-no-rm-rf", "text":"Never run rm -rf outside the workspace.", "kind":"limit", "tools":["bash"], "agents":["general"] } ], "timeout_ms":1500, "expected_revision":"01HPRO9a4b2c5f8e0d1a9b8d37091e3a" }'PUT /api/v1/agent/hooks
Section titled “PUT /api/v1/agent/hooks”Creates or updates a hook (hooks.upsert). Requires the begin-write nonce and a live session_id.
A hook’s identity is the triple event + matcher + command: the daemon requires a valid event and a non-empty command on every call, an absent matcher means the wildcard group, and a call whose triple already exists updates that entry in place (preserving keys it does not carry). Editing a hook’s command text therefore CREATES a second hook; delete the old triple to replace it.
Creating a NEW hook requires non-empty name and description (the daemon rejects an undocumented create). Updating an existing hook may omit them to preserve the current values.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route. |
realm | query | string | No | In-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route. |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id (hooks are session-scoped). |
nonce | string | Yes | The single-use write nonce from hooks.createWriteIntent minted for op:upsert plus this scope; the RPC fails closed without it. |
scope | string | No | Scope of the settings file to write (must match the nonce’s scope). |
event | string | Yes | Lifecycle event the hook fires on; part of the hook’s identity triple. Required, see Lifecycle events. |
matcher | string | No | Matcher selecting when the hook fires (a tool name, a Claude Code alias such as Bash/Edit, or a regex); part of the identity triple. Omit it (or pass "*") for the wildcard group. |
command | string | Yes | Command to run when the hook fires; part of the identity triple. Required and non-empty. A different command text addresses a DIFFERENT hook. |
timeout | integer | No | Per-fire timeout in whole seconds (optional; non-negative whole numbers only; a fractional or negative value is refused, anything above 86400 (24h) is clamped to it). Omit to keep an existing entry’s timeout; 0 or absent means the per-event default (60s for UserPromptSubmit and SessionEnd, 600s for every other event). |
name | string | No | Short human label shown in the Hooks tab. Conditionally required: a NEW hook (an identity triple not yet in the settings) is refused 400 without it; on an UPDATE omit it to preserve the stored value. |
description | string | No | Short description of what the hook does. Conditionally required exactly like name. |
{ "session_id": "67e89abc123def456789abcd890abcdef", "nonce": "01HDEF9a4b2c5f8e0d1a9b8d37091e3a", "scope": "project", "event": "PreToolUse", "matcher": "Bash", "command": "echo 'hook fired' >> /tmp/hooks.log", "timeout": 60, "name": "Log Bash commands", "description": "Append a line when a Bash command runs."}Response
Section titled “Response”{ "ok": true, "event": "PreToolUse", "matcher": "Bash", "command": "echo 'hook fired' >> /tmp/hooks.log", "scope": "project", "created": true}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "write_intent_invalid", "message": "stale write intent — reopen the editor"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
write_intent_invalid | Write intent invalid | The begin-write nonce was missing, expired, already spent, or minted for another op, session or scope. The nonce is consumed by the attempt, so a retry always needs a fresh one. Nothing was written. | Mint a new nonce with client.agent.hooks.createWriteIntent() for this exact op and scope, then retry. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge before the handler reads it. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
store_failed | Agent store failed | One of the agent’s own stores could not be read, parsed, locked or written, so the operation did not complete. The message names the store (for example “the agent’s settings file is damaged and was left unchanged”). A damaged store is left as it was rather than rewritten. | Retry once; if it persists, repair the named store inside the container. The agent’s log (GET /logs) records a “store failure” entry with the store, the failure kind (io, corrupt or lock) and a path-free cause such as “open: permission denied” or “invalid JSON at offset 12”; it does not name the file’s path. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.hooks.upsert({ session_id: '67e89abc123def456789abcd890abcdef', nonce: '01HDEF9a4b2c5f8e0d1a9b8d37091e3a', scope: 'project', event: 'PreToolUse', matcher: 'Bash', command: "echo 'hook fired' >> /tmp/hooks.log", timeout: 60, name: 'Log Bash commands', description: 'Append a line when a Bash command runs.',});curl -X PUT 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/hooks' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "session_id":"67e89abc123def456789abcd890abcdef", "nonce":"01HDEF9a4b2c5f8e0d1a9b8d37091e3a", "scope":"project", "event":"PreToolUse", "matcher":"Bash", "command":"echo '"'"'hook fired'"'"' >> /tmp/hooks.log", "timeout":60, "name":"Log Bash commands", "description":"Append a line when a Bash command runs." }'DELETE /api/v1/agent/hooks
Section titled “DELETE /api/v1/agent/hooks”Deletes a hook (hooks.delete). Requires the begin-write nonce (minted by hooks.createWriteIntent for op:delete) and a live session_id. Name the hook by the same identity triple hooks.upsert wrote: event (required and validated) + matcher + command.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route. |
realm | query | string | No | In-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route. |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id (hooks are session-scoped). |
nonce | string | Yes | The single-use write nonce from hooks.createWriteIntent minted for op:delete plus this scope; the RPC fails closed without it. |
scope | string | No | Scope of the settings file to write (must match the nonce’s scope). |
event | string | Yes | Lifecycle event of the hook to remove; required, see Lifecycle events. |
matcher | string | No | Matcher of the hook to remove, exactly as hooks.list reports it. Omit it (or pass "*") to address the wildcard group. |
command | string | No | Command text of the entry to remove (exact match). Omitting it removes EVERY command in the matched event+matcher group. |
{ "session_id": "67e89abc123def456789abcd890abcdef", "nonce": "01HDEF9a4b2c5f8e0d1a9b8d37091e3a", "scope": "project", "event": "PreToolUse", "matcher": "Bash", "command": "echo 'hook fired' >> /tmp/hooks.log"}Response
Section titled “Response”{ "ok": true, "event": "PreToolUse", "matcher": "Bash", "command": "echo 'hook fired' >> /tmp/hooks.log", "scope": "project", "removed": 1}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "write_intent_invalid", "message": "stale write intent — reopen the editor"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
write_intent_invalid | Write intent invalid | The begin-write nonce was missing, expired, already spent, or minted for another op, session or scope. The nonce is consumed by the attempt, so a retry always needs a fresh one. Nothing was written. | Mint a new nonce with client.agent.hooks.createWriteIntent() for this exact op and scope, then retry. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge before the handler reads it. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
store_failed | Agent store failed | One of the agent’s own stores could not be read, parsed, locked or written, so the operation did not complete. The message names the store (for example “the agent’s settings file is damaged and was left unchanged”). A damaged store is left as it was rather than rewritten. | Retry once; if it persists, repair the named store inside the container. The agent’s log (GET /logs) records a “store failure” entry with the store, the failure kind (io, corrupt or lock) and a path-free cause such as “open: permission denied” or “invalid JSON at offset 12”; it does not name the file’s path. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.hooks.delete({ session_id: '67e89abc123def456789abcd890abcdef', nonce: '01HDEF9a4b2c5f8e0d1a9b8d37091e3a', scope: 'project', event: 'PreToolUse', matcher: 'Bash', command: "echo 'hook fired' >> /tmp/hooks.log",});curl -X DELETE 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/hooks' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "session_id":"67e89abc123def456789abcd890abcdef", "nonce":"01HDEF9a4b2c5f8e0d1a9b8d37091e3a", "scope":"project", "event":"PreToolUse", "matcher":"Bash", "command":"echo '"'"'hook fired'"'"' >> /tmp/hooks.log" }'POST /api/v1/agent/hooks/toggle
Section titled “POST /api/v1/agent/hooks/toggle”Toggles a single hook’s enabled state (hooks.toggle). Requires the begin-write nonce (minted by hooks.createWriteIntent for op:toggle) and a live session_id.
There are two mutually exclusive ways to name the hook, which is why nothing past session_id/nonce is required unconditionally.
- A SHIPPED hook is addressed by
shipped_idplusenabled: enabling records it in the first settings layer; disabling REMOVES the key so it falls back to the catalogue default (disabled). A shipped toggle never carriesevent/matcher/command. - Every other hook is addressed by its full identity triple
eventplusmatcherpluscommand(all three, exactly ashooks.listreports them) plusdisabledfor the new state.
A toggle never authors a hook. An unmatched triple fails with "no hook found", and the entry’s timeout/name/description are preserved rather than rewritten.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route. |
realm | query | string | No | In-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route. |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id (hooks are session-scoped). |
nonce | string | Yes | The single-use write nonce from hooks.createWriteIntent minted for op:toggle plus this scope; the RPC fails closed without it. |
scope | string | No | Scope of the settings file to write (must match the nonce’s scope). |
shipped_id | string | No | Id of a SHIPPED (catalogue) hook to toggle. Selects the shipped branch; pair it with enabled and send no event/matcher/command. |
enabled | boolean | No | New state for the shipped hook named by shipped_id (true records it in the first settings layer; false removes the key, reverting to the catalogue default). Ignored on the ordinary-hook branch. |
event | string | No | Lifecycle event of the ordinary hook to toggle; required on that branch (with matcher + command), see Lifecycle events. |
matcher | string | No | Matcher of the ordinary hook to toggle, exactly as hooks.list reports it (omit, or "*", for the wildcard group). |
command | string | No | Command text of the ordinary hook to toggle (exact match); required on that branch. A toggle must resolve to an existing entry. |
disabled | boolean | No | New state for the ordinary hook: true disables the entry, false enables it. Absent reads as false (enable). Ignored on the shipped branch. |
{ "session_id": "67e89abc123def456789abcd890abcdef", "nonce": "01HDEF9a4b2c5f8e0d1a9b8d37091e3a", "scope": "project", "event": "PreToolUse", "matcher": "Bash", "command": "echo 'hook fired' >> /tmp/hooks.log", "disabled": true}Response
Section titled “Response”{ "ok": true, "event": "PreToolUse", "matcher": "Bash", "command": "echo 'hook fired' >> /tmp/hooks.log", "scope": "project", "disabled": true}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "write_intent_invalid", "message": "stale write intent — reopen the editor"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
write_intent_invalid | Write intent invalid | The begin-write nonce was missing, expired, already spent, or minted for another op, session or scope. The nonce is consumed by the attempt, so a retry always needs a fresh one. Nothing was written. | Mint a new nonce with client.agent.hooks.createWriteIntent() for this exact op and scope, then retry. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge before the handler reads it. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
store_failed | Agent store failed | One of the agent’s own stores could not be read, parsed, locked or written, so the operation did not complete. The message names the store (for example “the agent’s settings file is damaged and was left unchanged”). A damaged store is left as it was rather than rewritten. | Retry once; if it persists, repair the named store inside the container. The agent’s log (GET /logs) records a “store failure” entry with the store, the failure kind (io, corrupt or lock) and a path-free cause such as “open: permission denied” or “invalid JSON at offset 12”; it does not name the file’s path. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”The SDK reaches this operation through two methods:
client.agent.hooks.enable(...): setsenabled: truefor a shipped hook (shipped_id), ordisabled: falsefor an ordinary hook (eventandcommand).client.agent.hooks.disable(...): setsenabled: falsefor a shipped hook (shipped_id), ordisabled: truefor an ordinary hook (eventandcommand).
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.hooks.disable({ session_id: '67e89abc123def456789abcd890abcdef', nonce: '01HDEF9a4b2c5f8e0d1a9b8d37091e3a', scope: 'project', event: 'PreToolUse', matcher: 'Bash', command: "echo 'hook fired' >> /tmp/hooks.log",});curl -X POST 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/hooks/toggle' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "session_id":"67e89abc123def456789abcd890abcdef", "nonce":"01HDEF9a4b2c5f8e0d1a9b8d37091e3a", "scope":"project", "event":"PreToolUse", "matcher":"Bash", "command":"echo '"'"'hook fired'"'"' >> /tmp/hooks.log", "disabled":true }'POST /api/v1/agent/hooks/disable-all
Section titled “POST /api/v1/agent/hooks/disable-all”Disables (or re-enables) all hooks at once (hooks.set_disabled). Requires the begin-write nonce (minted by hooks.createWriteIntent for op:set_disabled) and a live session_id.
Send the state you want in value: true writes the hooks.enableAll / hooks.disableAll kill switch into the scope’s settings file; false CLEARS it. value is required here precisely because the daemon does not default it the way the operation name suggests. The field is read as a plain boolean, so an ABSENT value reads as false and the call DELETES the kill switch. A request that omits value therefore re-enables every hook under an operation called “disable all hooks”; always send the boolean explicitly.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route. |
realm | query | string | No | In-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route. |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id (hooks are session-scoped). |
nonce | string | Yes | The single-use write nonce from hooks.createWriteIntent minted for op:set_disabled plus this scope; the RPC fails closed without it. |
scope | string | No | Scope of the settings file to write (must match the nonce’s scope). |
value | boolean | Yes | New kill-switch state: true disables all hooks in this scope, false clears the setting and re-enables them. Required; an absent value is read as false (a re-enable). |
{ "session_id": "67e89abc123def456789abcd890abcdef", "nonce": "01HDEF9a4b2c5f8e0d1a9b8d37091e3a", "scope": "project", "value": true}Response
Section titled “Response”{ "ok": true, "scope": "project", "value": true, "disableAllHooks": true}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "write_intent_invalid", "message": "stale write intent — reopen the editor"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
write_intent_invalid | Write intent invalid | The begin-write nonce was missing, expired, already spent, or minted for another op, session or scope. The nonce is consumed by the attempt, so a retry always needs a fresh one. Nothing was written. | Mint a new nonce with client.agent.hooks.createWriteIntent() for this exact op and scope, then retry. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge before the handler reads it. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
store_failed | Agent store failed | One of the agent’s own stores could not be read, parsed, locked or written, so the operation did not complete. The message names the store (for example “the agent’s settings file is damaged and was left unchanged”). A damaged store is left as it was rather than rewritten. | Retry once; if it persists, repair the named store inside the container. The agent’s log (GET /logs) records a “store failure” entry with the store, the failure kind (io, corrupt or lock) and a path-free cause such as “open: permission denied” or “invalid JSON at offset 12”; it does not name the file’s path. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”The SDK reaches this operation through two methods:
client.agent.hooks.enableAll(...): setsvalue: false, which enables hooks again.client.agent.hooks.disableAll(...): setsvalue: true, which disables every hook.
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.hooks.disableAll({ session_id: '67e89abc123def456789abcd890abcdef', nonce: '01HDEF9a4b2c5f8e0d1a9b8d37091e3a', scope: 'project',});curl -X POST 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/hooks/disable-all' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{"session_id":"67e89abc123def456789abcd890abcdef","nonce":"01HDEF9a4b2c5f8e0d1a9b8d37091e3a","scope":"project","value":true}'Testing and trust
Section titled “Testing and trust”POST /api/v1/agent/hooks/test
Section titled “POST /api/v1/agent/hooks/test”Test-fires a hook command (hooks.test). This EXECUTES an arbitrary command now. Requires a live session_id. Which fields you must send depends on the variant, so only session_id is unconditionally required.
- Passing
shipped_idselects the shipped-hook DRY RUN: it evaluates that catalogue hook’s trigger against a sample command (commandplusexit_code, both optional) and reports whether it would dispatch. It executes nothing. - Every other call requires a valid
eventand DOES execute: passcommandto run an unsaved command inline (test-before-save, with an optionaltimeout), or omitcommandto fire the SAVED hooks matchingeventplusmatch_value.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route. |
realm | query | string | No | In-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route. |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id (hooks are session-scoped). |
shipped_id | string | No | Id of a SHIPPED (catalogue) hook to DRY-RUN. Selects the predicate-only branch; nothing is executed and event is not consulted. |
event | string | No | Lifecycle event to fire. Required on every branch except the shipped dry-run, see Lifecycle events. |
match_value | string | No | Value the saved hooks’ matchers are evaluated against (for example the tool name for PreToolUse). Ignored when command supplies an inline hook. |
command | string | No | On the ordinary branch: an unsaved command to dry-run INLINE (this executes it) instead of the saved matching hooks. On the shipped branch: the sample command the trigger predicate is evaluated against (defaults to a representative failing invocation). |
timeout | integer | No | Timeout in whole seconds for an inline command (positive integers only; a fractional or non-positive value is ignored rather than truncated). Hard-capped at 60s. |
exit_code | integer | No | Exit code of the sample command on the shipped dry-run branch (default 2). |
payload | object | No | Extra fields merged into the hook’s stdin JSON. The daemon stamps session_id, cwd, hook_event_name and hook_test itself; values supplied here for those keys are overwritten. |
{ "session_id": "67e89abc123def456789abcd890abcdef", "event": "PreToolUse", "match_value": "Bash", "command": "echo 'hook fired' >> /tmp/hooks.log", "timeout": 30}Response
Section titled “Response”{ "ok": true, "event": "PreToolUse", "match_value": "Bash", "command": "echo 'hook fired' >> /tmp/hooks.log", "exit_code": 0, "stdout": "", "stderr": "", "duration_ms": 42, "fired": true}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "approval_policy_unsatisfiable", "message": "hooks.test executes a command; not available while the session's approval policy is \"always\""}| Error Code | Title | Description | Resolution |
|---|---|---|---|
approval_policy_unsatisfiable | Approval policy refuses a hook test | The session’s approval policy is “always”, and a command-hook test executes a command without a per-invocation decision, so the test is refused and nothing runs. The shipped-hook dry run (shipped_id) executes nothing and is not refused. | Test the hook from a session whose approval policy is “default”, or use the shipped-hook dry run. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge before the handler reads it. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”The SDK reaches this operation through two methods:
client.agent.hooks.run(...): runs an ordinary hook;eventis required andshipped_idis not accepted.client.agent.hooks.test(...): dry-runs a shipped hook;shipped_idis required.
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.hooks.run({ session_id: '67e89abc123def456789abcd890abcdef', event: 'PreToolUse', match_value: 'Bash', command: "echo 'hook fired' >> /tmp/hooks.log", timeout: 30,});curl -X POST 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/hooks/test' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "session_id":"67e89abc123def456789abcd890abcdef", "event":"PreToolUse", "match_value":"Bash", "command":"echo '"'"'hook fired'"'"' >> /tmp/hooks.log", "timeout":30 }'POST /api/v1/agent/hooks/trust/ack
Section titled “POST /api/v1/agent/hooks/trust/ack”Acknowledges the hook-trust prompt (hooks.trust_ack). Requires a live session_id and the hash being acknowledged.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route. |
realm | query | string | No | In-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route. |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id (hooks are session-scoped). |
hash | string | Yes | The trust hash being acknowledged, from hooks.list’ trust.hash. Required and non-empty; a hash that no longer matches the live hook config is rejected (anti-TOCTOU). |
high_risk | boolean | No | Acknowledge the elevated-risk grant rather than the ordinary one. The grant KIND is still derived server-side from the reviewed snapshot; this flag cannot widen it. |
{ "session_id": "67e89abc123def456789abcd890abcdef", "hash": "f4c2e1a9b8d37091e3a4b2c5f8e0d1a9"}Response
Section titled “Response”{ "ok": true, "hash": "f4c2e1a9b8d37091e3a4b2c5f8e0d1a9", "high_risk": false, "ack_at": "2024-09-14T12:00:00Z"}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge before the handler reads it. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
store_failed | Agent store failed | One of the agent’s own stores could not be read, parsed, locked or written, so the operation did not complete. The message names the store (for example “the agent’s settings file is damaged and was left unchanged”). A damaged store is left as it was rather than rewritten. | Retry once; if it persists, repair the named store inside the container. The agent’s log (GET /logs) records a “store failure” entry with the store, the failure kind (io, corrupt or lock) and a path-free cause such as “open: permission denied” or “invalid JSON at offset 12”; it does not name the file’s path. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.hooks.trust({ session_id: '67e89abc123def456789abcd890abcdef', hash: 'f4c2e1a9b8d37091e3a4b2c5f8e0d1a9',});curl -X POST 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/hooks/trust/ack' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{"session_id":"67e89abc123def456789abcd890abcdef","hash":"f4c2e1a9b8d37091e3a4b2c5f8e0d1a9"}'