Session gates and approvals
Section titled “Session gates and approvals”A gate parks a turn until a human answers it. There are two kinds: a confirm gate asks for an approve or deny decision on a tool call or directory access, and a question gate asks for a free-form or structured answer. This page covers the four surfaces that surround a gate: listing and answering parked gates, the session’s approval policy (mode, lock, per-tool rules, lease, and YOLO), and the tool-call rules that determine which calls actually park. Answering a confirm gate on an always policy also requires the approver lease; that surface is documented on the leases page.
List pending gates
Section titled “List pending gates”GET /api/v1/agent/gates
Section titled “GET /api/v1/agent/gates”List every gate this gateway can answer that is waiting for a human, across the sessions GET /sessions shows. Each entry is one live session’s confirm or question, or, on a helper-gates session, one helper’s gate (a background task’s pending gate is its helper’s gate, with task_id). Order is oldest parked first, then session_id, then generation. Pagination is 1-based; an omitted or zero limit, or one above 100, is served as 100, and meta.limit echoes the value used. meta.omitted counts gates that changed or resolved during the read.
Answer one with POST /sessions/{session_id}/confirm or /answer, sending its gate_id. A gate is only listed while this gateway holds its session live; a session held by another client (the TUI) is not listed until it is attached here.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
include_system | query | boolean | No | When true, also list the gates of daemon-owned system/resident sessions (as sessions.list does). |
realm | query | string | No | The realm to list: “global” (gates not tied to a realm), a 24-hex realm id, or “all” for every realm this login serves. Also accepted as the X-Hoody-Realm header, except “all”. Omitted, the agent’s current realm. |
page | query | integer | No | 1-based page number for pagination. |
limit | query | integer | No | Maximum items per page, at most 100. A value of 0, an omitted value, or a value above 100 is served as 100, and meta.limit echoes the value used. |
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/write resolves against. |
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. |
Response
Section titled “Response”The pending gates.
{ "items": [ { "agent": "builder", "gate": { "gate_cause": "rules", "gate_id": 3, "generation": 0, "human_only": false, "lease_required": false, "params": { "command": "git push origin main" }, "risk": "unknown", "rules": { "outcome": "ask", "rule_ids": [ "ask-git" ] }, "status": "parked", "tool_name": "bash", "type": "confirm" }, "gate_id": "gate-9f2c4e1ab07d3355-3", "generation": 3, "kind": "confirm", "parked_at": "2026-09-28T10:15:04.512Z", "realm": "global", "rules": { "outcome": "ask", "rule_ids": [ "ask-git" ] }, "session_id": "9f2c4e1a-5b7d-4c3e-8a21-6d0f3b9e7c55" } ], "meta": { "limit": 100, "omitted": 0, "page": 1, "total": 1 }}| Field | Type | Description |
|---|---|---|
items | array | The page of items, built by the gateway. |
items[].session_id | string | The session the gate is parked on. |
items[].realm | string | The session’s realm: "global" for a session not tied to a realm, else its 24-hex realm id. Pass it as X-Hoody-Realm (or ?realm=) when answering the gate. |
items[].gate_id | string | The answerable gate id: send it as gate_id on POST /sessions/{session_id}/confirm (kind confirm) or /answer (kind question). Scoped to this gateway’s session incarnation. |
items[].generation | integer | The gateway’s gate generation (optional on the answer; a mismatch is refused as stale_gate). |
items[].kind | string | What the gate asks for: confirm (approve or deny a tool call) or question (an answer). |
items[].helper_id | string | Present on a helper’s gate: the spawn_agent helper that parked it. |
items[].parent_tool_call_id | string | Present on a helper’s gate: the lead’s spawn_agent tool call that started the helper. |
items[].task_id | string | Present on a background helper’s gate: its task id. |
items[].agent | string | The session’s agent. Omitted when the session record names none. |
items[].parked_at | string | RFC3339 time this gateway parked the gate. A re-attach parks it again with a new gate_id and time. |
items[].rules | object | Present when tool-call rules asked for the gate: {rule_ids, outcome}. |
items[].gate | object | The daemon’s redacted description of the gate, as GET /sessions/{id}/state shows it. |
meta | object | Pagination metadata. |
meta.total | integer | Every gate parked when the list was read, across all pages. |
meta.page | integer | The 1-based page served. Always present. |
meta.limit | integer | The page size used, at most 100. Always present. |
meta.omitted | integer | Gates in this page window that changed or resolved during the read. |
{ "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 or 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": "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, so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
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 });
for await (const item of client.agent.gates.listIterator({ page: 1, limit: 100 })) { console.log(item);}curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/gates?page=1&limit=100" \ -H "Authorization: Bearer $HOODY_TOKEN"Read the approval policy
Section titled “Read the approval policy”GET /api/v1/agent/sessions/{id}/approval
Section titled “GET /api/v1/agent/sessions/{id}/approval”Returns the session’s effective approval policy: the mode (default or always), the monotonic lock, the revision, the waiver posture (yolo, auto_write, auto_dir_access, backend_auto_approving), the session permission rules, and the approver lease state. The response ETag is the revision; send it as If-Match on a change. Works for a live or persisted session.
The event.permission_rules frames pushed on the stream are snapshots of the same rule set. Each one states what the rules are at that moment and follows a successful durable write (a write whose commit fails rolls back and broadcasts nothing), so a client converges on the frame’s contents rather than on having asked for a change.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override. |
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 (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only or no-realm routes. |
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). |
Response
Section titled “Response”The session’s approval policy. ETag = revision.
{ "mode": "default", "locked": false, "revision": 3, "yolo": false, "auto_write": false, "auto_dir_access": false, "backend_auto_approving": false, "rules": [ { "tool": "bash", "decision": "allow" } ], "lease": { "held": false, "holder": null, "generation": 0, "expires_at": null, "epoch": "ep-7f3a1c" }, "epoch": "ep-7f3a1c", "capabilities": { "always": true, "locked": true, "lease": true }}| Field | Type | Description |
|---|---|---|
mode | string | "default" (automatic waivers may satisfy gates) or "always" (one explicit decision per side-effecting invocation). Sessionless headless runs, sessionless tool runs and management operations are outside any session’s policy. |
locked | boolean | Whether the policy is frozen for the session’s lifetime (monotonic; never unlocked). |
revision | integer | Monotonic policy revision, the ETag value. |
yolo | boolean | Whether YOLO auto-approve is armed. |
auto_write | boolean | Whether automatic file-write permission is on (a CLI-default posture an HTTP attach may inherit); always false under "always". |
auto_dir_access | boolean | Whether automatic directory access is on. |
backend_auto_approving | boolean | Whether a delegated backend auto-approves its own tools. |
rules | array | Session permission rules. |
rules[].tool | string | Rule key (a tool name, or tool:action). |
rules[].decision | string | allow or deny. |
lease | object | Approver lease state: {held, holder, generation, expires_at, epoch}. The capability itself is never shown here. |
epoch | string | The daemon execution epoch (an opaque id). Changes on restart; leases and parked gates do not survive it. |
changed | boolean | PUT only: whether the request changed anything. |
capabilities | object | What this daemon supports: {always, locked, lease}. |
{ "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 or global-no-realm RPC. | 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. | 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. | 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 });
const policy = await client.agent.sessions.getApproval('{session_id}');curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/approval" \ -H "Authorization: Bearer $HOODY_TOKEN"Inspect which rules apply
Section titled “Inspect which rules apply”GET /api/v1/agent/sessions/{id}/rules/applies
Section titled “GET /api/v1/agent/sessions/{id}/rules/applies”Returns the tool-call rules a live session checks for one agent’s calls, from the same selection the rules check itself runs. A rule without tools skips read-only tools, a rule with tools covers only those, and a rule with agents covers only those agents’ calls. agent defaults to the session’s own agent; name a helper’s or workflow step’s agent to see what its calls are checked against. With tool, the reply’s checked rules (applies_as: checked) are exactly the rules sent to Jev for that call; without it, every checked rule that covers the agent’s calls to some tool. Guidance and playbook rules are listed too, with applies_as: prompt. fail_closed_reason says when every covered call fails closed right now (trust_hold, jev_disabled, or jev_no_key).
A dormant session answers 404 not_found.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
agent | query | string | No | Agent name to ask about (default: the session’s own agent). |
tool | query | string | No | Tool name to ask about (default: any tool). |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override. |
X-Hoody-Container | header | string | No | Per-request bound remote container. |
X-Hoody-Realm | header | string | No | Per-request realm selector. |
realm | query | string | No | Per-request realm selector (in:query alias of the X-Hoody-Realm header). |
Response
Section titled “Response”The applying rules.
{ "agent": "builder", "tool": "bash", "read_only": false, "active": true, "count": 2, "rules": [ { "id": "ask-git", "text": "Ask before running git push.", "kind": "check_in", "applies_as": "checked", "tools": ["bash"], "agents": [], "source": "/etc/hoody/settings.json" } ], "prompt_off": false, "prompt_bytes": 184, "prompt_budget_bytes": 8192, "prompt_over_budget": []}| Field | Type | Description |
|---|---|---|
agent | string | The acting agent asked about: the agent query value, else the session’s own agent. |
tool | string | The tool asked about. Omitted when no tool was named. |
read_only | boolean | Whether the named tool is read-only. Omitted when no tool was named. |
active | boolean | Whether the session has rules that are checked at all (limit or check_in). |
count | integer | len(rules): the rules that apply to the agent, checked and prompt together. |
rules | array | The rules that apply to the agent, in settings order. |
rules[].id | string | The rule id. |
rules[].text | string | The rule in plain words. |
rules[].kind | string | limit, check_in, guidance, or playbook. |
rules[].applies_as | string | Output only, derived from kind. checked = Jev checks covered tool calls; prompt = its text is in the covered agents’ system prompt. |
rules[].tools | array | The tools the rule is limited to; empty = every tool that is not read-only. Always empty for guidance and playbook rules. |
rules[].agents | array | The agents whose calls the rule covers; empty = every agent. |
rules[].source | string | The settings file that declared the rule. |
prompt_off | boolean | Whether prompt_blocks drops the team-rules block for this agent. |
prompt_bytes | integer | The size in bytes of the rule lines the agent’s team-rules block carries. 0 when prompt_off is true. |
prompt_budget_bytes | integer | The most rule-line bytes one agent’s team-rules block carries (8192). |
prompt_over_budget | array | The guidance and playbook rules that cover the agent but did not fit prompt_budget_bytes. |
fail_closed_reason | string | Present when rules apply and none can be checked right now: trust_hold, jev_disabled, or jev_no_key. |
{ "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 or global-no-realm RPC. | 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. | 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. | 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 });
const rules = await client.agent.sessions.listApplicableRules('{session_id}', { agent: 'builder', tool: 'bash' });curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/rules/applies?agent=builder&tool=bash" \ -H "Authorization: Bearer $HOODY_TOKEN"Answer a parked question
Section titled “Answer a parked question”POST /api/v1/agent/sessions/{id}/answer
Section titled “POST /api/v1/agent/sessions/{id}/answer”Answers a parked ask-the-user question. Provide answer or text for a free-form reply, or answers (a map) for a structured multi-field question. gate_id and generation are echoes of the parked gate; a mismatch is 409 stale_gate. 409 no_pending_gate when nothing is parked; 409 gate_already_answered when an earlier request already won; 409 gate_type_mismatch when the parked gate is a confirm gate (use /confirm for those); 409 gate_cancelled when the turn was cancelled while the answer was in flight (do not retry this answer).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override. |
X-Hoody-Container | header | string | No | Per-request bound remote container. |
X-Hoody-Realm | header | string | No | Per-request realm selector. |
realm | query | string | No | Per-request realm selector (in:query alias of the X-Hoody-Realm header). |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
gate_id | string | No | Echo of the parked gate id, as published on the frame that parked it. Valid only for the session in the path. Ids are unique per session incarnation, so an id from before a re-attach never matches. |
generation | integer | No | Optional echo of the parked gate generation. Restarts when the session is re-attached, so it is not an identity on its own. |
answer | string | No | Free-form answer text. When it is absent or blank, text is used in its place. It may be empty when text or answers carries the answer. |
text | string | No | Answer text used when answer is absent or blank; ignored otherwise. |
answers | object | No | Structured per-field answers for a multi-field question. |
{ "gate_id": "gate-3f9a1c2b7d4e6f80-2", "generation": 2, "answer": "Yes, push to main."}Response
Section titled “Response”Answered.
{ "status": "ok", "resolved": { "gate_id": "gate-3f9a1c2b7d4e6f80-2", "outcome": "answered" }}| Field | Type | Description |
|---|---|---|
status | string | "ok" on success. |
resolved | object | The daemon’s resolution of the gate, when it acknowledged within the wait. |
replayed | boolean | True when a retried decision naming the same gate got the original acknowledgement back. Nothing was forwarded a second time. |
session_scope_applied | boolean | Present when a session-wide grant could not be applied; see note. |
note | string | Present alongside session_scope_applied: why the wider grant did not apply. |
{ "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 or global-no-realm RPC. | 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": "stale_gate", "message": "the gate id/generation does not match the parked gate", "details": { "gate_id": "gate-3f9a1c2b7d4e6f80-2", "generation": 2, "type": "question" }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
no_pending_gate | No pending gate | No confirm/question gate is parked for this session, so there is nothing to answer. | Dispatch a turn and answer the gate it parks; check the pending gate via GET /sessions/{id}. |
stale_gate | Stale gate | The supplied gate_id/generation does not match the currently parked gate. | Answer the gate in details (echo its gate_id and generation), or re-read the pending gate when details are absent. |
gate_already_answered | Gate already answered | The parked gate was already answered by an earlier request (first valid answer wins). | Wait for the next gate; do not re-answer. |
gate_type_mismatch | Gate type mismatch | The parked gate is a different type than this endpoint answers. | Use the endpoint matching the parked gate type (/confirm for a confirm gate, /answer for a question gate). |
gate_cancelled | Gate cancelled | The gate was resolved without consuming this decision: the turn was cancelled or the session ended while the decision was in flight. The decision was NOT applied. | Do not retry this decision: the gate it answered no longer exists. Re-read GET /sessions/{id}/state and answer the gate parked there, if any. |
{ "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 size limit (default 8 MiB). | 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. | 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": "decision_unconfirmed", "message": "the daemon did not acknowledge the decision within the wait", "details": { "gate_id": "gate-…", "generation": 1 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request. | Honor Retry-After and retry. |
decision_unconfirmed | Decision unconfirmed | The decision was forwarded but the daemon acknowledged neither its consumption nor a refusal within the wait. It may still apply. | GET /sessions/{id}/state: if the gate is gone the decision applied; otherwise retry it. |
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.gates.answer('{session_id}', { gate_id: 'gate-3f9a1c2b7d4e6f80-2', generation: 2, answer: 'Yes, push to main.',});curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/answer" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "gate_id": "gate-3f9a1c2b7d4e6f80-2", "generation": 2, "answer": "Yes, push to main." }'Propose answers for a parked question
Section titled “Propose answers for a parked question”POST /api/v1/agent/sessions/{id}/answer:assist
Section titled “POST /api/v1/agent/sessions/{id}/answer:assist”Runs a one-shot helper-model call that proposes answers for the parked question. The real answer still travels via /answer; the daemon never answers itself. The suggestion arrives later as event.question_suggestion, with the job id from the 202 ack. 409 no_pending_gate when no question is parked; 409 assist_in_flight when another assist is already running for this session.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override. |
X-Hoody-Container | header | string | No | Per-request bound remote container. |
X-Hoody-Realm | header | string | No | Per-request realm selector. |
realm | query | string | No | Per-request realm selector (in:query alias of the X-Hoody-Realm header). |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
mode | string | No | Suggestion mode (default "suggest"). |
model | string | No | Helper model override; empty uses the configured helper. |
gen | integer | No | Generation counter to correlate the suggestion event (echoed as event.question_suggestion gen). |
{ "mode": "suggest", "gen": 1}Response
Section titled “Response”Suggestion job dispatched. Fetch its result via GET /jobs/{id}/result.
{ "job_id": "job-9f2c4e1ab07d3355-7", "session_id": "9f2c4e1a-5b7d-4c3e-8a21-6d0f3b9e7c55"}| Field | Type | Description |
|---|---|---|
job_id | string | Gateway-minted job id for the helper call. GET /jobs/{id}/result returns the suggestion under result once the job is terminal; it is null while the job is still running. |
session_id | string | The session whose parked question is being answered (echo of the path id). |
{ "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 or global-no-realm RPC. | 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": "realm_not_allowed", "message": "realm 0123456789abcdef01234567 is not allowed for this realm-limited login"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
realm_not_allowed | Realm not allowed for this login | The agent is logged in with a token limited to some realms, and this session’s realm is not one of them. | Continue in a session of a realm the login serves, or log the agent in with a token that covers this session’s realm. |
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": "assist_in_flight", "message": "an answer:assist job is already running for this session"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
no_pending_gate | No pending gate | No confirm/question gate is parked for this session, so there is nothing to answer. | Dispatch a turn and answer the gate it parks; check the pending gate via GET /sessions/{id}. |
assist_in_flight | Assist in flight | An answer:assist suggestion job is already running for this session; only one in-flight assist per session is allowed. | Wait for the prior suggestion (observe event.question_suggestion or poll the job), 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 size limit (default 8 MiB). | 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. | 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. | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve. | 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 });
const assist = await client.agent.gates.suggest('{session_id}', { mode: 'suggest', gen: 1 });const jobId = assist.data.job_id;curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/answer:assist" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mode": "suggest", "gen": 1 }'Answer a parked confirm gate
Section titled “Answer a parked confirm gate”POST /api/v1/agent/sessions/{id}/confirm
Section titled “POST /api/v1/agent/sessions/{id}/confirm”Approves or denies a parked tool or directory confirmation. A 200 means the agent consumed this decision; nothing else is a 200. The required approved boolean is mandatory: a confirm without a boolean approved is rejected 400 approved_required and the gate stays parked. On a session that requires approval on every action (mode: "always"), gate_id and generation are required too, and once the session’s approver lease was minted, every decision also carries it in X-Hoody-Approver-Lease.
session_scope: true remembers the decision for the rest of the session (with approved: true the tool stops asking, with approved: false it is refused without asking). Offer allow-for-session only when the gate’s event.confirm_request carried offer_session_allow. Under a locked policy the wider grant is refused, but the one-shot decision still applies and the reply says so (session_scope_applied: false, note). trust_container: true accepts the gate’s exec_trust offer (also only on the gate’s offer).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
X-Hoody-Approver-Lease | header | string | No | The approver-lease capability returned by POST /sessions/{id}/approver-lease. Required on every decision on an "always" session whose lease was minted; the daemon verifies it at decision consumption. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override. |
X-Hoody-Container | header | string | No | Per-request bound remote container. |
X-Hoody-Realm | header | string | No | Per-request realm selector. |
realm | query | string | No | Per-request realm selector (in:query alias of the X-Hoody-Realm header). |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
approved | boolean | Yes | true to approve, false to deny. There is no default; a missing, null, or non-boolean value is rejected 400. |
gate_id | string | No | Echo of the parked gate id. Required on a session that requires approval on every action: a confirm without it is rejected 400. |
generation | integer | No | Echo of the parked gate generation. Optional on a default-policy session; required, and non-zero, on a session that requires approval on every action. |
persist_dirs | boolean | No | Persist an approved directory grant to settings.json (WS↔REST parity). |
session_scope | boolean | No | Remember this decision for the rest of the session. With approved: true the tool stops asking, with approved: false it is refused without asking. Offer allow-for-session only when the gate’s event.confirm_request carried offer_session_allow. |
trust_container | boolean | No | With approved: true, accept the gate’s exec_trust offer. Ignored when the gate carried no offer or the approval policy is locked. |
request_id | string | No | Optional caller id for this decision, at most 64 characters from A-Z, a-z, 0-9, ., _, and -. A later 409 gate_already_resolved for the gate echoes it as details.request_id. |
lease_generation | integer | No | The approver-lease generation the decision was made under. Forwarded as is; omitted or 0 sends none. |
{ "gate_id": "gate-9f2c4e1ab07d3355-3", "generation": 3, "approved": true, "session_scope": false}Response
Section titled “Response”Answered.
{ "status": "ok", "resolved": { "gate_id": "gate-9f2c4e1ab07d3355-3", "outcome": "answered" }}| Field | Type | Description |
|---|---|---|
status | string | "ok" on success. |
resolved | object | The daemon’s resolution of the gate (event.gate_resolved) that consumed this decision. |
session_scope_applied | boolean | Present (false) when the decision applied once but its session-wide grant could not be applied; see note. |
note | string | Present alongside session_scope_applied: why the wider grant did not apply. |
{ "code": "approved_required", "message": "/confirm must carry approved: true to approve or false to deny"}| 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 or global-no-realm RPC. | Omit the realm header on this route, or open a session to scope by realm. |
approved_required | Decision missing | The confirm did not carry a boolean approved. An omitted or null decision never approves or denies anything, and the gate stays parked. | Send approved: true to approve or approved: false to deny. |
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": "gate_already_resolved", "message": "the gate was already resolved; this decision was not applied", "details": { "decision": { "approved": true, "persist_dirs": false, "session_scope": false, "trust_container": false }, "gate_id": "gate-9f2c4e1ab07d3355-3", "generation": 3, "outcome": "answered", "request_id": "ui-7f3a" }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
gate_decision_pending | Gate decision pending | A decision for this gate was sent to the agent and its outcome is not known yet. | Retry the same decision naming the gate. |
gate_already_resolved | Gate already resolved | The gate already ended, so this decision was not applied. details.outcome says how it ended. | Do not resend the decision. Read details.decision for what was decided, then re-read GET /sessions/{id}/state and answer the gate parked there, if any. |
no_pending_gate | No pending gate | No confirm/question gate is parked for this session, so there is nothing to answer. | Dispatch a turn and answer the gate it parks; check the pending gate via GET /sessions/{id}. |
stale_gate | Stale gate | The supplied gate_id/generation does not match the currently parked gate. | Answer the gate in details (echo its gate_id and generation), or re-read the pending gate when details are absent. |
gate_type_mismatch | Gate type mismatch | The parked gate is a different type than this endpoint answers. | Use the endpoint matching the parked gate type (/confirm for a confirm gate, /answer for a question gate). |
decision_incomplete | Decision incomplete | The session’s approval policy is "always": a decision must carry an explicit approved, the exact gate_id, and its generation. Nothing was decided and the gate stays parked. | Re-send the confirm with approved, the parked gate’s gate_id, and its generation. |
approver_lease_required | Approver lease required | This session’s approver lease was minted, so every decision must present the current capability in X-Hoody-Approver-Lease. | Acquire the lease (POST /sessions/{id}/approver-lease) and re-issue the confirmation with X-Hoody-Approver-Lease. |
approver_lease_invalid | Approver lease invalid | The presented capability does not verify for this session (wrong session, a fenced generation, or a daemon restart). | Re-acquire the lease (POST /sessions/{id}/approver-lease). |
{ "code": "approver_lease_expired", "message": "the approver lease expired"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
approver_lease_expired | Approver lease expired | The presented lease lapsed; an expired lease never authorises a decision. | Re-acquire the lease (POST /sessions/{id}/approver-lease). |
{ "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 size limit (default 8 MiB). | 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. | 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. | Honor Retry-After and retry. |
SDK usage
Section titled “SDK usage”The SDK reaches this operation through two methods:
client.agent.gates.approve(...): setsapproved: true.client.agent.gates.deny(...): setsapproved: false.
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.gates.approve('{session_id}', { gate_id: 'gate-9f2c4e1ab07d3355-3', generation: 3, session_scope: false,});curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/confirm" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "gate_id": "gate-9f2c4e1ab07d3355-3", "generation": 3, "approved": true, "session_scope": false }'Set approval mode and lock
Section titled “Set approval mode and lock”PUT /api/v1/agent/sessions/{id}/approval
Section titled “PUT /api/v1/agent/sessions/{id}/approval”Changes the session’s approval mode ("default" or "always") and/or its lock. Use If-Match (the ETag from GET, or * for unconditional) to make the change conditional. A stale If-Match is 412 (the response ETag carries the current revision). A locked policy is 409 approval_policy_locked; an unknown mode, a delegated session, an unlock attempt, or weakening an inherited lock is 409 approval_policy_unsatisfiable; a busy session is 409 session_busy (the policy only changes at quiescence); a failed durable commit is 503 policy_commit_failed (nothing acknowledged). Emits event.approval_policy_changed to every attached client.
"always" makes every side-effecting Hoody-visible invocation require one explicit decision naming its gate. Nested executors that cannot park a decision are refused (subagents, workflow runs and resumes, the orchestrator, foreign CLIs, run_todo, test_hook); the user’s configured hook commands are skipped, each disclosed as event.hook_run {decision:"skipped"}; configured MCP servers are not started; stdin to a background bash process is refused. Machine-confirmed execution paths (a composite or fusion model’s winner-commit run in writes mode, subagents, workflow tool steps, auto-reply) have their side-effecting invocations refused under "always" (error code approval_policy_unsatisfiable); reads still pass.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
If-Match | header | string | No | Conditional-request precondition: the ETag from GET /sessions/{id}/approval (a quoted policy revision, e.g. "3") or *. A mismatch is 412 precondition_failed. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override. |
X-Hoody-Container | header | string | No | Per-request bound remote container. |
X-Hoody-Realm | header | string | No | Per-request realm selector. |
realm | query | string | No | Per-request realm selector (in:query alias of the X-Hoody-Realm header). |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
mode | string | No | "default" or "always". Omit to keep the current mode. |
locked | boolean | No | Freeze the policy for the session’s lifetime (monotonic; never unlocked). Omit to keep the current lock. |
{ "mode": "always", "locked": true}Response
Section titled “Response”The updated policy. ETag = new revision.
{ "mode": "always", "locked": true, "revision": 4, "yolo": false, "auto_write": false, "auto_dir_access": false, "backend_auto_approving": false, "rules": [], "lease": { "held": false, "holder": null, "generation": 0, "expires_at": null, "epoch": "ep-7f3a1c" }, "epoch": "ep-7f3a1c", "changed": true, "capabilities": { "always": true, "locked": true, "lease": true }}| Field | Type | Description |
|---|---|---|
mode | string | "default" or "always". |
locked | boolean | Whether the policy is frozen for the session’s lifetime (monotonic; never unlocked). |
revision | integer | Monotonic policy revision, the ETag value. |
yolo | boolean | Whether YOLO auto-approve is armed. |
auto_write | boolean | Whether automatic file-write permission is on; always false under "always". |
auto_dir_access | boolean | Whether automatic directory access is on. |
backend_auto_approving | boolean | Whether a delegated backend auto-approves its own tools. |
rules | array | Session permission rules. |
rules[].tool | string | Rule key (a tool name, or tool:action). |
rules[].decision | string | allow or deny. |
lease | object | Approver lease state: {held, holder, generation, expires_at, epoch}. |
epoch | string | The daemon execution epoch (an opaque id). |
changed | boolean | PUT only: whether the request changed anything. |
capabilities | object | What this daemon supports: {always, locked, lease}. |
{ "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 or global-no-realm RPC. | 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_locked", "message": "the approval policy is locked"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
approval_policy_locked | Approval policy locked | The session’s approval policy is locked for its lifetime: the mode, the lock, YOLO, allow rules, session-scoped remembered approvals and auto-reply arming cannot be changed. | Create a new session with the posture you need; a locked policy is never unlocked. |
approval_policy_unsatisfiable | Approval policy unsatisfiable | The approval assertion cannot be honoured: an attach asserted a posture the shared session does not have, the backend cannot park on a decision, or the session’s posture is already open. | Attach without the assertion (or with the session’s actual posture), or create a fresh session with the policy you need. |
session_busy | Session busy | The session is already live in another connection (single-writer-per-session). | Close the other attach, or attach a fork instead. |
{ "code": "precondition_failed", "message": "the approval policy revision changed; refetch and retry with the current ETag", "details": { "revision": 4 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
precondition_failed | Precondition failed | If-Match named a policy revision that is no longer current. The response ETag carries the current revision (also under details.revision). | Refetch GET /sessions/{id}/approval (or use the ETag on this response) and 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 size limit (default 8 MiB). | 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. | 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": "policy_commit_failed", "message": "the approval policy could not be persisted; nothing was acknowledged"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request. | Honor Retry-After and retry. |
policy_commit_failed | Policy commit failed | The daemon could not durably write the policy, so the change was NOT acknowledged (a locked policy is never acknowledged unlocked). | Retry; if it persists, check the daemon’s session store (disk or permissions). |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve. | 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.sessions.updateApproval('{session_id}', { mode: 'always', locked: true }, { IfMatch: '"3"' });curl -X PUT "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/approval" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: \"3\"" \ -d '{ "mode": "always", "locked": true }'Set a session permission rule
Section titled “Set a session permission rule”PUT /api/v1/agent/sessions/{id}/approval/rules/{tool}
Section titled “PUT /api/v1/agent/sessions/{id}/approval/rules/{tool}”Upserts one session permission rule for {tool}: {"decision":"allow"|"deny"}. One rule per request (never an array). An allow is refused 409 approval_policy_locked on a locked policy and 409 approval_policy_active on an "always" policy (allow rules are never consulted there); a deny is always accepted. Optional If-Match (the ETag from GET /approval): a stale one is 412 with the current ETag; the answer carries the new ETag. Emits event.permission_rules and event.approval_policy_changed.
A rule write is durable-or-nothing: if the durable write fails the rule is rolled back, the answer is 503 policy_commit_failed, and nothing is broadcast, because nothing was announced to retract. Every event.permission_rules frame therefore carries rules that are persisted. The frame is still a snapshot to converge on rather than a receipt for this request: another client’s write, or an in-session decision, produces one too.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
tool | path | string | Yes | The tool. |
If-Match | header | string | No | Conditional-request precondition: the ETag from GET /sessions/{id}/approval (a quoted policy revision, e.g. "3") or *. A mismatch is 412 precondition_failed. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override. |
X-Hoody-Container | header | string | No | Per-request bound remote container. |
X-Hoody-Realm | header | string | No | Per-request realm selector. |
realm | query | string | No | Per-request realm selector (in:query alias of the X-Hoody-Realm header). |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
decision | string | Yes | allow or deny. |
{ "decision": "allow"}Response
Section titled “Response”The session’s permission rules after the change.
{ "status": "ok", "rules": [ { "tool": "bash", "decision": "allow" } ]}| Field | Type | Description |
|---|---|---|
status | string | "ok". |
rules | array | Session permission rules. |
rules[].tool | string | Rule key (a tool name, or tool:action). |
rules[].decision | string | allow or deny. |
{ "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 or global-no-realm RPC. | 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_locked", "message": "the approval policy is locked"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
approval_policy_locked | Approval policy locked | The session’s approval policy is locked for its lifetime. | Create a new session with the posture you need; a locked policy is never unlocked. |
approval_policy_active | Approval policy active | This session requires an explicit decision on every action, so an automatic-answer alias (the auto_approve gate posture, arming YOLO, an allow rule) is refused. | Answer each gate through POST /sessions/{id}/confirm, or change the policy first (PUT /sessions/{id}/approval) if it is not locked. |
{ "code": "precondition_failed", "message": "the approval policy revision changed; refetch and retry with the current ETag", "details": { "revision": 4 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
precondition_failed | Precondition failed | If-Match named a policy revision that is no longer current. The response ETag carries the current revision (also under details.revision). | Refetch GET /sessions/{id}/approval (or use the ETag on this response) and 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 size limit (default 8 MiB). | 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. | 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": "policy_commit_failed", "message": "the approval policy could not be persisted; nothing was acknowledged"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request. | Honor Retry-After and retry. |
policy_commit_failed | Policy commit failed | The daemon could not durably write the policy, so the change was NOT acknowledged (a locked policy is never acknowledged unlocked). | Retry; if it persists, check the daemon’s session store (disk or permissions). |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve. | 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.sessions.setApprovalRule('{session_id}', 'bash', { decision: 'allow' });curl -X PUT "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/approval/rules/bash" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "decision": "allow" }'Remove a session permission rule
Section titled “Remove a session permission rule”DELETE /api/v1/agent/sessions/{id}/approval/rules/{tool}
Section titled “DELETE /api/v1/agent/sessions/{id}/approval/rules/{tool}”Clears the session permission rule for {tool} (re-arming the per-call prompt). Refused 409 approval_policy_locked on a locked policy (rules cannot be cleared there). Optional If-Match, as on the PUT (412 when stale). Emits event.permission_rules and event.approval_policy_changed. A rule write is durable-or-nothing: if the durable write fails the rule is rolled back, the answer is 503 policy_commit_failed, and nothing is broadcast.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
tool | path | string | Yes | The tool. |
If-Match | header | string | No | Conditional-request precondition: the ETag from GET /sessions/{id}/approval (a quoted policy revision, e.g. "3") or *. A mismatch is 412 precondition_failed. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override. |
X-Hoody-Container | header | string | No | Per-request bound remote container. |
X-Hoody-Realm | header | string | No | Per-request realm selector. |
realm | query | string | No | Per-request realm selector (in:query alias of the X-Hoody-Realm header). |
This endpoint takes no body.
Response
Section titled “Response”The session’s permission rules after the change.
{ "status": "ok", "rules": []}| Field | Type | Description |
|---|---|---|
status | string | "ok". |
rules | array | Session permission rules. |
rules[].tool | string | Rule key (a tool name, or tool:action). |
rules[].decision | string | allow or deny. |
{ "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 or global-no-realm RPC. | 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_locked", "message": "the approval policy is locked"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
approval_policy_locked | Approval policy locked | The session’s approval policy is locked for its lifetime. | Create a new session with the posture you need; a locked policy is never unlocked. |
approval_policy_active | Approval policy active | This session requires an explicit decision on every action, so an automatic-answer alias is refused. | Answer each gate through POST /sessions/{id}/confirm, or change the policy first (PUT /sessions/{id}/approval) if it is not locked. |
{ "code": "precondition_failed", "message": "the approval policy revision changed; refetch and retry with the current ETag", "details": { "revision": 4 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
precondition_failed | Precondition failed | If-Match named a policy revision that is no longer current. The response ETag carries the current revision (also under details.revision). | Refetch GET /sessions/{id}/approval (or use the ETag on this response) and retry. |
{ "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. | 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": "policy_commit_failed", "message": "the approval policy could not be persisted; nothing was acknowledged"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request. | Honor Retry-After and retry. |
policy_commit_failed | Policy commit failed | The daemon could not durably write the policy, so the change was NOT acknowledged (a locked policy is never acknowledged unlocked). | Retry; if it persists, check the daemon’s session store (disk or permissions). |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve. | 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.sessions.deleteApprovalRule('{session_id}', 'bash');curl -X DELETE "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/approval/rules/bash" \ -H "Authorization: Bearer $HOODY_TOKEN"Arm or disarm YOLO
Section titled “Arm or disarm YOLO”PATCH /api/v1/agent/sessions/{id}/yolo
Section titled “PATCH /api/v1/agent/sessions/{id}/yolo”Flips the session’s YOLO auto-approve on or off, the same power as the TUI’s lightning toggle, including clearing actions the agent otherwise reserves for a person (a caller that can reach this API is treated as having that permission). A locked policy refuses it 409 approval_policy_locked and an "always" policy refuses it 409 approval_policy_active, rather than dropping it silently. Returns the applied {yolo}. Emits event.yolo_mode.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override. |
X-Hoody-Container | header | string | No | Per-request bound remote container. |
X-Hoody-Realm | header | string | No | Per-request realm selector. |
realm | query | string | No | Per-request realm selector (in:query alias of the X-Hoody-Realm header). |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | Yes | true to arm auto-approve, false to disarm. |
{ "enabled": true}Response
Section titled “Response”Applied YOLO state.
{ "status": "ok", "yolo": true}| Field | Type | Description |
|---|---|---|
status | string | "ok". |
yolo | boolean | The applied auto-approve state. |
forwarded | boolean | true when the command was delivered but not acknowledged within the wait; observe event.yolo_mode. |
{ "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 or global-no-realm RPC. | 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_locked", "message": "the approval policy is locked"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
approval_policy_locked | Approval policy locked | The session’s approval policy is locked for its lifetime. | Create a new session with the posture you need; a locked policy is never unlocked. |
approval_policy_active | Approval policy active | This session requires an explicit decision on every action, so arming YOLO is refused. | Answer each gate through POST /sessions/{id}/confirm, or change the policy first (PUT /sessions/{id}/approval) if it is not locked. |
{ "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 size limit (default 8 MiB). | 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. | 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. | Honor Retry-After and retry. |
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.sessions.setYolo('{session_id}', { enabled: true });curl -X PATCH "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/yolo" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "enabled": true }'