Skip to content
AGH RuntimeHooks

Hook Declaration Format

Reference for declaring AGH hooks, matchers, subprocess executors, ordering, and runtime execution behavior.

Audience
Operators running durable agent work
Focus
Hooks guidance shaped for scanability, day-two clarity, and operator context.

A hook declaration binds one event to one executor. The matcher decides whether the hook is eligible; the executor receives the payload and writes a patch. AGH normalizes declarations from config files, agent definitions, skills, and extension manifests into one deterministic pipeline.

Declaration Sources

SourceWhere to declareNotes
config[[hooks.declarations]] in AGH config TOMLGlobal and workspace config declarations are loaded through workspace resolution.
agent_definitionhooks: YAML or [[hooks]] TOML frontmatter in AGENT.mdAGH scopes the matcher to that agent name. A conflicting matcher.agent_name is rejected.
skillmetadata.agh.hooks in SKILL.md frontmatterMarketplace skill hooks are blocked unless allowed by skills.allowed_marketplace_hooks.
config with extension metadata[[resources.hooks]] in extension.toml or extension.jsonExtension hooks run from the extension root and receive metadata.extension.
nativeDaemon code onlyUsed by built-in runtime hooks with Go executors. Users should not declare native hooks.

Field Reference

FieldRequiredMeaning
nameyesStable hook name used in catalog and run history.
eventyesOne event from the event catalog.
modenoasync by default; set sync only for sync-eligible events.
requirednoIf true, the hook must be sync; execution failure aborts the dispatch.
prioritynoHigher values run earlier inside the same source group.
timeoutnoGo duration such as 5s or 250ms. Subprocess hooks default to 5s.
matchernoFamily-specific filters. Empty matcher fields match all values.
commandsubprocess hooksExecutable name or path. Inferred executor kind is subprocess when command is set.
argsnoArguments passed directly to the executable. AGH does not implicitly run a shell.
envnoExtra environment variables for the hook process.
executor.kindno for subprocess commandExplicit executor kind. Use subprocess for user-declared hooks.
executor.command, executor.args, executor.envnoNested executor form. Do not mix nested executor fields with top-level command, args, or env.

The source and skill_source values in catalog output are assigned by AGH from the declaration location. They are not fields you normally write.

Execution Model

Rendering diagram...

Sync hooks can mutate the runtime payload. Async hooks observe the sync-patched payload and record outcomes without applying patches back to the caller.

AGH orders hooks by source, then priority, then name:

  1. Source order: native, config, agent_definition, skill.
  2. Higher priority runs before lower priority.
  3. Skill hooks with the same priority are ordered by skill source: bundled, marketplace, user, additional, workspace.
  4. Ties sort by hook name.

Default priorities are:

SourceDefault priority
native1000
config500
agent_definition100
skill0

Sync hooks run serially. A sync hook sees the payload patched by earlier sync hooks for the same event. Async hooks are queued only after the sync chain completes without a denial or required-hook failure. Async execution defaults to 4 workers, queue capacity 64, and 10s shutdown drain timeout. If the async queue is full, the hook run is recorded as dropped.

AGH limits nested hook dispatch depth to 3. A sync dispatch beyond that depth fails with a dispatch-depth error. Async hooks beyond the depth limit are recorded as skipped.

Matchers

String matcher fields accept exact values or Go path.Match wildcards: *, ?, and character classes such as [abc]. Empty strings and * match all values. Invalid wildcard patterns fail declaration validation. tool_read_only is a boolean exact match.

Each event family accepts only its own matcher fields:

FamilyAllowed matcher fields
sessionagent_name, workspace_id, workspace_root, session_type
inputagent_name, workspace_id, workspace_root, input_class
promptagent_name, workspace_id, workspace_root, input_class
eventagent_name, acp_event_type, turn_id
automationagent_name, workspace_id
agentagent_name, workspace_id, workspace_root
turnagent_name, workspace_id, workspace_root, input_class
tooltool_name, tool_namespace, tool_read_only
permissiontool_name, decision_class
messagemessage_role, message_delta_type
contextcompaction_reason, compaction_strategy
coordinatoragent_name, workspace_id, workspace_root, matcher.autonomy.task_id, matcher.autonomy.run_id, matcher.autonomy.workflow_id, matcher.autonomy.coordination_channel_id, matcher.autonomy.coordinator_session_id
task.runagent_name, workspace_id, matcher.autonomy.task_id, matcher.autonomy.run_id, matcher.autonomy.workflow_id, matcher.autonomy.coordination_channel_id, matcher.autonomy.release_reason
spawnagent_name, workspace_id, workspace_root, matcher.autonomy.task_id, matcher.autonomy.run_id, matcher.autonomy.workflow_id, matcher.autonomy.coordination_channel_id, matcher.autonomy.parent_session_id, matcher.autonomy.root_session_id, matcher.autonomy.child_session_id, matcher.autonomy.spawn_role

agent_type exists in the internal matcher struct, but no current event family accepts it. Setting it in a declaration fails validation.

Autonomy correlation fields are nested under matcher.autonomy in config, agent, skill, and extension declarations. They use the same exact or wildcard string matching as other string matchers:

[[hooks.declarations]]
name = "block-low-priority-claim"
event = "task.run.pre_claim"
mode = "sync"
required = true
command = "./hooks/claim-policy"

[hooks.declarations.matcher]
workspace_id = "ws_checkout"

[hooks.declarations.matcher.autonomy]
workflow_id = "release-*"
coordination_channel_id = "coord-*"

Executors

KindWho should use itBehavior
subprocessUser, config, agent, skill, and extension hook declarationsRuns a local executable. JSON payload goes to stdin. JSON patch comes from stdout.
nativeBuilt-in daemon hooks onlyBinds a Go executor supplied by the daemon. Rejected for non-native sources.
wasmReservedThe kind exists, but execution currently returns not implemented.

Subprocess details:

  • AGH resolves the command with exec.LookPath; bare commands use PATH.
  • The command is not run through a shell unless you explicitly use sh, bash, or another shell as the command.
  • Environment is restricted to a safe allowlist plus explicit env entries.
  • Stdout and stderr capture is capped at 8 KiB; truncated output ends with ...[truncated].
  • A timed-out process gets a graceful termination window before AGH kills it.

Complete Example: Config Tool Gate

This config hook inspects write-capable tool calls before they execute. The hook process receives a ToolPreCallPayload on stdin and must write a ToolCallPatch to stdout.

[[hooks.declarations]]
name = "block-secret-file-edits"
event = "tool.pre_call"
mode = "sync"
required = true
priority = 700
timeout = "3s"

[hooks.declarations.matcher]
tool_namespace = "fs"
tool_read_only = false

[hooks.declarations.executor]
kind = "subprocess"
command = "/usr/bin/env"
args = ["bash", "./hooks/block-secret-file-edits.sh"]
env = { POLICY = "secret-files" }

./hooks/block-secret-file-edits.sh:

#!/usr/bin/env bash
set -euo pipefail

payload="$(cat)"

case "$payload" in
  *'".env"'*|*'"id_rsa"'*)
    printf '{"deny":true,"deny_reason":"Protected secret file path."}\n'
    ;;
  *)
    printf '{}\n'
    ;;
esac

Complete Example: Agent Prompt Hook

Agent definition hooks live in AGENT.md frontmatter. AGH automatically scopes them to the agent name, so this declaration behaves as if matcher.agent_name = "reviewer" were set.

---
name: reviewer
provider: codex
hooks:
  - name: add-review-context
    event: prompt.post_assemble
    mode: sync
    timeout: 5s
    command: /usr/bin/env
    args: ["node", "./hooks/add-review-context.mjs"]
    matcher:
      input_class: user
---

Review code changes with emphasis on behavior, tests, and operational risk.

./hooks/add-review-context.mjs:

const chunks = [];
for await (const chunk of process.stdin) {
  chunks.push(chunk);
}

const payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
const prompt = payload.prompt ?? "";

process.stdout.write(
  JSON.stringify({
    prompt: `Review policy: cite file paths and blocking risks first.\n\n${prompt}`,
  }) + "\n"
);

Complete Example: Extension-Bundled Hook

Extension manifests can package hook declarations with the extension. Relative commands stay inside the extension root; {{config_dir}} expands to that root and {{env:NAME}} expands from the daemon process environment.

[extension]
name = "prompt-enhancer"
version = "0.1.0"
description = "Adds workspace context to assembled prompts"
min_agh_version = "0.5.0"

[[resources.hooks]]
name = "workspace-context"
event = "prompt.post_assemble"
mode = "sync"
timeout = "5s"

[resources.hooks.matcher]
input_class = "user"

[resources.hooks.executor]
kind = "subprocess"
command = "node"
args = ["dist/index.js", "hook", "prompt_post_assemble"]
env = { LOG_LEVEL = "info" }

The command still follows the hook executor contract: read payload JSON from stdin and write a patch JSON object to stdout. A separate long-running extension subprocess is optional and is documented in Develop Extensions.

HTTP And Agent Workflows

HTTP and agent executors are not implemented hook kinds today. Use one of these patterns instead:

NeedCurrent pattern
Call an HTTP endpoint from a hookUse a subprocess hook that runs curl, node, python, or another client.
Start or inspect AGH sessions from extension codeBuild a subprocess extension and request Host API actions such as sessions/list or sessions/create.
Run a long-lived platform integrationBuild an extension subprocess; keep hooks as short stdin/stdout commands when they need to patch runtime payloads.

Example HTTP wrapper:

[[hooks.declarations]]
name = "notify-session-created"
event = "session.post_create"
mode = "async"
command = "/usr/bin/env"
args = ["bash", "-lc", "curl -fsS -X POST \"$WEBHOOK_URL\" -H 'Content-Type: application/json' --data-binary @- >/dev/null && printf '{}\\n'"]
env = { WEBHOOK_URL = "https://hooks.example.test/agh/session-created" }

Observability

Hook runs are persisted per session. Each run records hook name, event, source, mode, duration, outcome, dispatch depth, optional patch, error text, required flag, and timestamp.

Possible outcomes:

OutcomeMeaning
appliedThe hook ran and its patch was accepted.
deniedThe hook returned a denial patch.
failedThe executor failed or produced an invalid patch.
skippedThe hook was not run because dispatch depth was exceeded.
droppedThe async queue was full.
rejectedA guard rejected the patch, such as a permission escalation attempt.

Inspect the resolved catalog and execution history through the parallel CLI and tool surfaces:

agh hooks list --workspace checkout --event tool.pre_call
agh hooks info block-secret-file-edits --workspace checkout
agh hooks runs --session sess_9f4a --event tool.pre_call --last 20 -o json

Agents can read the same projection through agh__hooks_list, agh__hooks_info, agh__hooks_events, and agh__hooks_runs. Agents may also mutate config-backed or overlay-backed declarations through agh__hooks_create, agh__hooks_update, agh__hooks_delete, agh__hooks_enable, and agh__hooks_disable. Source-owned hooks remain structurally immutable through agent tools; the daemon returns HOOK_SOURCE_IMMUTABLE instead of attempting the mutation.

Related references:

On this page