Skip to content
Hoody.com

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.

The event field on every operation that accepts it must be one of the following values (the daemon rejects any value outside the enum):

  • Notification
  • PostToolUse
  • PreCompact
  • PreToolUse
  • SessionEnd
  • SessionStart
  • Stop
  • SubagentStart
  • SubagentStop
  • UserPromptSubmit

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.

NameInTypeRequiredDescription
session_idquerystringNoLive session id (hooks are session-scoped; required by the daemon RPC).
X-Hoody-CwdheaderstringNoPer-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read resolves.
X-Hoody-RealmheaderstringNoPer-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.
realmquerystringNoPer-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.
{
"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
}
}
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' });

Reloads the hook configuration from disk (hooks.reload). Pass a live session_id to also receive the reloaded summary.

NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route.
realmquerystringNoIn-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route.

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"
}
{
"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
}
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' });

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.

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.

NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read resolves.
X-Hoody-RealmheaderstringNoPer-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.
realmquerystringNoPer-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.
{
"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
}
}
FieldTypeDescription
rulesobjectThe rules block of client.agent.hooks.list() without its session half.
rules.livearrayThe rules as they load from the scopes’ files now, merged in load order (user, project, local), checked and prompt kinds together.
rules.live[].idstringThe rule id.
rules.live[].textstringThe rule in plain words.
rules.live[].kindstringlimit (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_asstringOutput 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[].toolsarrayThe 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[].agentsarrayThe agents whose calls the rule covers; empty means every agent.
rules.live[].sourcestringThe settings file that declared the rule.
rules.timeout_msintegerThe effective Jev budget for one rules check in ms (the scopes’ rules_timeout_ms, else the default 3000).
rules.scopesarrayOne 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[].scopestringThe settings scope: user, project, or local.
rules.scopes[].revisionstringThe scope file’s rules revision: pass it as hooks.setRules’ expected_revision.
rules.exec_trustarrayThe 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_idstringThe container id (24 hex) the grant is keyed by; a container name is never used.
rules.exec_trust[].realmstringThe realm the grant applies in: _global or a realm id (24 hex).
rules.exec_trust[].namestringThe container name when the grant was made, for display (at most 128 characters).
rules.exec_trust[].granted_atstringWhen the grant was made (RFC3339).
rules.exec_trust[].sourcestringcard (an exec card answered with trust_container) or rules_panel (hooks.setRules, the default for a row sent without one).
rules.warningsarrayEvery 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_warningsarrayThe 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_completebooleantrue 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.
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();

Every guarded write follows the same pattern:

  1. Call hooks.createWriteIntent with session_id, the target op, and scope. Receive a one-shot nonce that binds to that tuple. The supported op values are upsert, delete, toggle, set_disabled, and rules_set (used by hooks.setRules).
  2. Call the matching write (upsert, delete, toggle, set_disabled, or rules_set) with session_id, scope, and the returned nonce. The RPC fails closed without the nonce.

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.

NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless write resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route.
realmquerystringNoIn-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route.
FieldTypeRequiredDescription
session_idstringYesLive session id (hooks are session-scoped).
opstringYesThe 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.
scopestringYesScope 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"
}
{
"nonce": "01HDEF9a4b2c5f8e0d1a9b8d37091e3a",
"op": "upsert",
"scope": "project",
"session_id": "67e89abc123def456789abcd890abcdef",
"expires_at": "2024-09-14T12:00:00Z"
}
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',
});

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.

NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless write resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route.
realmquerystringNoIn-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route.
FieldTypeRequiredDescription
session_idstringYesLive session id (hooks are session-scoped).
noncestringYesThe single-use write nonce from hooks.createWriteIntent minted for op:rules_set plus this scope; the RPC fails closed without it.
scopestringNoScope of the settings file to write (must match the nonce’s scope).
rulesarrayNoThe 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[].idstringYesStable rule id: 1-64 characters of a-z, 0-9, _ or -, starting with a letter or digit.
rules[].textstringYesThe rule in plain words, at most 500 characters, no control characters.
rules[].kindstringYeslimit (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[].toolsarrayNoOptional: 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[].agentsarrayNoOptional: 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_msintegerNoOptional: 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_trustarrayNoOptional: 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_idstringYesThe container id (24 hex) the grant is keyed by; a container name is never used.
exec_trust[].realmstringYesThe realm the grant applies in: _global or a realm id (24 hex).
exec_trust[].namestringNoThe container name when the grant was made, for display (at most 128 characters).
exec_trust[].granted_atstringNoWhen the grant was made (RFC3339).
exec_trust[].sourcestringNocard (an exec card answered with trust_container) or rules_panel (hooks.setRules, the default for a row sent without one).
expected_revisionstringNoOptional: 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"
}
{
"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
}
}
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',
});

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.

NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless write resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route.
realmquerystringNoIn-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route.
FieldTypeRequiredDescription
session_idstringYesLive session id (hooks are session-scoped).
noncestringYesThe single-use write nonce from hooks.createWriteIntent minted for op:upsert plus this scope; the RPC fails closed without it.
scopestringNoScope of the settings file to write (must match the nonce’s scope).
eventstringYesLifecycle event the hook fires on; part of the hook’s identity triple. Required, see Lifecycle events.
matcherstringNoMatcher 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.
commandstringYesCommand to run when the hook fires; part of the identity triple. Required and non-empty. A different command text addresses a DIFFERENT hook.
timeoutintegerNoPer-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).
namestringNoShort 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.
descriptionstringNoShort 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."
}
{
"ok": true,
"event": "PreToolUse",
"matcher": "Bash",
"command": "echo 'hook fired' >> /tmp/hooks.log",
"scope": "project",
"created": true
}
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.',
});

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.

NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless write resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route.
realmquerystringNoIn-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route.
FieldTypeRequiredDescription
session_idstringYesLive session id (hooks are session-scoped).
noncestringYesThe single-use write nonce from hooks.createWriteIntent minted for op:delete plus this scope; the RPC fails closed without it.
scopestringNoScope of the settings file to write (must match the nonce’s scope).
eventstringYesLifecycle event of the hook to remove; required, see Lifecycle events.
matcherstringNoMatcher of the hook to remove, exactly as hooks.list reports it. Omit it (or pass "*") to address the wildcard group.
commandstringNoCommand 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"
}
{
"ok": true,
"event": "PreToolUse",
"matcher": "Bash",
"command": "echo 'hook fired' >> /tmp/hooks.log",
"scope": "project",
"removed": 1
}
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",
});

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_id plus enabled: 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 carries event/matcher/command.
  • Every other hook is addressed by its full identity triple event plus matcher plus command (all three, exactly as hooks.list reports them) plus disabled for 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.

NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless write resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route.
realmquerystringNoIn-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route.
FieldTypeRequiredDescription
session_idstringYesLive session id (hooks are session-scoped).
noncestringYesThe single-use write nonce from hooks.createWriteIntent minted for op:toggle plus this scope; the RPC fails closed without it.
scopestringNoScope of the settings file to write (must match the nonce’s scope).
shipped_idstringNoId of a SHIPPED (catalogue) hook to toggle. Selects the shipped branch; pair it with enabled and send no event/matcher/command.
enabledbooleanNoNew 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.
eventstringNoLifecycle event of the ordinary hook to toggle; required on that branch (with matcher + command), see Lifecycle events.
matcherstringNoMatcher of the ordinary hook to toggle, exactly as hooks.list reports it (omit, or "*", for the wildcard group).
commandstringNoCommand text of the ordinary hook to toggle (exact match); required on that branch. A toggle must resolve to an existing entry.
disabledbooleanNoNew 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
}
{
"ok": true,
"event": "PreToolUse",
"matcher": "Bash",
"command": "echo 'hook fired' >> /tmp/hooks.log",
"scope": "project",
"disabled": true
}

The SDK reaches this operation through two methods:

  • client.agent.hooks.enable(...): sets enabled: true for a shipped hook (shipped_id), or disabled: false for an ordinary hook (event and command).
  • client.agent.hooks.disable(...): sets enabled: false for a shipped hook (shipped_id), or disabled: true for an ordinary hook (event and command).
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",
});

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.

NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless write resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route.
realmquerystringNoIn-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route.
FieldTypeRequiredDescription
session_idstringYesLive session id (hooks are session-scoped).
noncestringYesThe single-use write nonce from hooks.createWriteIntent minted for op:set_disabled plus this scope; the RPC fails closed without it.
scopestringNoScope of the settings file to write (must match the nonce’s scope).
valuebooleanYesNew 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
}
{
"ok": true,
"scope": "project",
"value": true,
"disableAllHooks": true
}

The SDK reaches this operation through two methods:

  • client.agent.hooks.enableAll(...): sets value: false, which enables hooks again.
  • client.agent.hooks.disableAll(...): sets value: 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',
});

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_id selects the shipped-hook DRY RUN: it evaluates that catalogue hook’s trigger against a sample command (command plus exit_code, both optional) and reports whether it would dispatch. It executes nothing.
  • Every other call requires a valid event and DOES execute: pass command to run an unsaved command inline (test-before-save, with an optional timeout), or omit command to fire the SAVED hooks matching event plus match_value.
NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route.
realmquerystringNoIn-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route.
FieldTypeRequiredDescription
session_idstringYesLive session id (hooks are session-scoped).
shipped_idstringNoId of a SHIPPED (catalogue) hook to DRY-RUN. Selects the predicate-only branch; nothing is executed and event is not consulted.
eventstringNoLifecycle event to fire. Required on every branch except the shipped dry-run, see Lifecycle events.
match_valuestringNoValue the saved hooks’ matchers are evaluated against (for example the tool name for PreToolUse). Ignored when command supplies an inline hook.
commandstringNoOn 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).
timeoutintegerNoTimeout 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_codeintegerNoExit code of the sample command on the shipped dry-run branch (default 2).
payloadobjectNoExtra 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
}
{
"ok": true,
"event": "PreToolUse",
"match_value": "Bash",
"command": "echo 'hook fired' >> /tmp/hooks.log",
"exit_code": 0,
"stdout": "",
"stderr": "",
"duration_ms": 42,
"fired": true
}

The SDK reaches this operation through two methods:

  • client.agent.hooks.run(...): runs an ordinary hook; event is required and shipped_id is not accepted.
  • client.agent.hooks.test(...): dry-runs a shipped hook; shipped_id is 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,
});

Acknowledges the hook-trust prompt (hooks.trust_ack). Requires a live session_id and the hash being acknowledged.

NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope (the .hoody project layer / record cwd / tool+workflow cwd). Required by routes that resolve a cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on this route.
realmquerystringNoIn-query alias of the X-Hoody-Realm header (read only when the header is absent). Rejected (400 realm_scope_unsupported) on this route.
FieldTypeRequiredDescription
session_idstringYesLive session id (hooks are session-scoped).
hashstringYesThe 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_riskbooleanNoAcknowledge 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"
}
{
"ok": true,
"hash": "f4c2e1a9b8d37091e3a4b2c5f8e0d1a9",
"high_risk": false,
"ack_at": "2024-09-14T12:00:00Z"
}
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',
});