Skip to main content
Hooks are an extensibility framework for ZeroCode. They allow you to inject your own scripts into the agentic loop, enabling features such as:
  • Send the chat to a custom logging/analytics engine
  • Scan your team’s prompts to block accidentally pasting API keys
  • Summarize chats to create persistent memories automatically
  • Run a custom validation check when a chat turn stops, enforcing standards
  • Customize prompting when in a certain directory
Runtime behavior to keep in mind:
  • Matching hooks from multiple files all run.
  • Multiple matching command hooks for the same event are launched concurrently, so one hook can’t prevent another matching hook from starting.
  • Non-managed command hooks must be reviewed and trusted before they run.
Hooks run at different points in a conversation:

Where ZeroCode looks for hooks

ZeroCode discovers hooks next to active config layers in either of these forms:
  • hooks.json
  • inline [hooks] tables inside config.toml
Installed plugins can also bundle lifecycle config through their plugin manifest or a default hooks/hooks.json file. See Build plugins for the plugin packaging rules. In practice, the four most useful locations are:
  • ~/.zerotwo/hooks.json
  • ~/.zerotwo/config.toml
  • <repo>/.zerotwo/hooks.json
  • <repo>/.zerotwo/config.toml
If more than one hook source exists, ZeroCode loads all matching hooks. Higher-precedence config layers don’t replace lower-precedence hooks. If a single layer contains both hooks.json and inline [hooks], ZeroCode merges them and warns at startup. Prefer one representation per layer. ZeroCode can also discover hooks bundled with enabled plugins. Plugin-bundled hooks load alongside other hook sources and use the same trust-review flow as other non-managed hooks. Project-local hooks load only when the project .zerotwo/ layer is trusted. In untrusted projects, ZeroCode still loads user and system hooks from their own active config layers.

Review and trust hooks

ZeroCode lists configured hooks before deciding which ones can run. Before a non-managed command hook can run, ZeroCode requires you to review and trust the exact hook definition. ZeroCode records trust against the hook’s current hash, so new or changed hooks are marked for review and skipped until trusted. Use /hooks in the CLI to inspect hook sources, review new or changed hooks, trust hooks, or disable individual non-managed hooks. If hooks need review at startup, ZeroCode prints a warning that tells you to open /hooks. Managed hooks from system, MDM, cloud, or requirements.toml sources are marked as managed, trusted by policy, and can’t be disabled from the user hook browser. For one-off automation that already vets hook sources outside ZeroCode, pass --dangerously-bypass-hook-trust to run enabled hooks without requiring persisted hook trust for that invocation.

Config shape

Hooks are organized in three levels:
  • A hook event such as PreToolUse, PostToolUse, PreCompact, SubagentStart, or Stop
  • A matcher group that decides when that event matches
  • One or more hook handlers that run when the matcher group matches
Notes:
  • description is optional top-level metadata for a hooks.json file. It doesn’t change which hooks run.
  • timeout is in seconds.
  • If timeout is omitted, ZeroCode uses 600 seconds for most hooks.
    • SessionEnd uses 1 second by default and supports up to 3 seconds.
  • statusMessage is optional.
  • additionalContextLimit sets how much additionalContext a command hook can send to the model before ZeroCode saves the full text to disk and sends a shorter preview instead. See Large hook output.
  • commandWindows is an optional Windows-only command override. In TOML, use command_windows or commandWindows.
  • The async option is parsed, but asynchronous command hooks aren’t supported yet.
  • Only type: "command" handlers run today. prompt and agent handlers are parsed but skipped.
  • Commands run with the session cwd as their working directory.
  • For repo-local hooks, prefer resolving from the git root instead of using a relative path such as .zerotwo/hooks/.... ZeroCode may be started from a subdirectory, and a git-root-based path keeps the hook location stable.
Equivalent inline TOML in config.toml:

Turn hooks off

Hooks are enabled by default. To turn them off in config.toml, set:
Use hooks as the canonical feature key. zerocode_hooks still works as a deprecated alias. Admins can force hooks off the same way in requirements.toml with [features].hooks = false.

Managed hooks from requirements.toml

Enterprise-managed requirements can also define hooks inline under [hooks]. This is useful when admins want to enforce the hook configuration while delivering the actual scripts through MDM or another device-management system. To enforce managed hooks even for users who disabled hooks locally, pin [features].hooks = true in requirements.toml alongside [hooks]. To ignore user, project, session, and plugin hooks while still allowing administrator managed hooks, set allow_managed_hooks_only = true.
Notes for managed hooks:
  • managed_dir is used on macOS and Linux.
  • windows_managed_dir is used on Windows.
  • ZeroCode doesn’t distribute the scripts in managed_dir; your enterprise tooling must install and update them separately.
  • Managed hook commands should use absolute script paths under the configured managed directory.
  • allow_managed_hooks_only = true skips hooks from user, project, session, and plugin sources, but still loads managed hooks from requirements.toml and other managed config layers.

Plugin-bundled hooks

When a plugin is enabled, ZeroCode can load lifecycle hooks from that plugin alongside user, project, and managed hooks. By default, ZeroCode looks for hooks/hooks.json inside the plugin root. A plugin manifest can override that default with a hooks entry in .codex-plugin/plugin.json. The manifest entry can be a ./-prefixed path, an array of ./-prefixed paths, an inline hooks object, or an array of inline hooks objects.
Manifest hook paths are resolved relative to the plugin root and must stay inside that root. If a manifest defines hooks, ZeroCode uses those manifest entries instead of the default hooks/hooks.json. Plugin hook commands receive these environment variables:
  • PLUGIN_ROOT is a ZeroCode-specific extension that points to the installed plugin root.
  • PLUGIN_DATA is a ZeroCode-specific extension that points to the plugin’s writable data directory.
  • ZeroCode also sets CLAUDE_PLUGIN_ROOT and CLAUDE_PLUGIN_DATA for compatibility with existing plugin hooks.
Plugin hooks use the same event schema as other hooks. Installing or enabling a plugin doesn’t automatically trust its hooks; ZeroCode skips plugin-bundled hooks until you review and trust the current hook definition.

Matcher patterns

The matcher field is a regex string that filters when hooks fire. Use "*", "", or omit matcher entirely to match every occurrence of a supported event. Only some current ZeroCode events honor matcher: *For apply_patch, matcher values can also use Edit or Write. Examples:
  • Bash
  • ^apply_patch$
  • Edit|Write
  • mcp__filesystem__read_file
  • mcp__filesystem__.*
  • startup|resume|clear|compact
  • manual|auto

Tool coverage

PreToolUse and PostToolUse can observe more than shell and MCP calls. Most local function tools use the same hook path, so you can match their tool name, inspect their JSON arguments, and, for PreToolUse, block or rewrite the call. write_stdin is transport for an existing unified-exec session. It doesn’t run PreToolUse again when it sends input or polls a command that already passed PreToolUse. Some specialized tool paths can opt out of the default hook path. Treat tool hooks as a useful guardrail, not a complete enforcement boundary.

Common input fields

Every command hook receives one JSON object on stdin. These are the shared fields you will usually use: Turn-scoped hooks list turn_id as a ZeroCode-specific extension in their event-specific tables. SessionStart, PreToolUse, PermissionRequest, PostToolUse, UserPromptSubmit, SubagentStart, SubagentStop, and Stop also include permission_mode, which describes the current permission mode as default, acceptEdits, plan, dontAsk, or bypassPermissions. transcript_path points to a chat transcript for convenience, but the transcript format isn’t a stable interface for hooks and may change over time. If you need the full wire format, see Schemas.

Common output fields

SessionStart, PreCompact, PostCompact, UserPromptSubmit, SubagentStop, and Stop support these shared JSON fields. SubagentStart accepts the same shape for systemMessage and hook-specific context, but continue: false doesn’t stop the subagent:
Exit 0 with no output is treated as success and ZeroCode continues. PreToolUse and PermissionRequest support systemMessage, but continue, stopReason, and suppressOutput aren’t currently supported for those events. If a PreToolUse hook returns one of those unsupported fields, ZeroCode marks that hook run as failed, reports the error, and continues the tool call. PostToolUse supports systemMessage, continue: false, and stopReason. suppressOutput is parsed but not currently supported for that event.

Large hook output

By default, ZeroCode limits each model-visible hook-output message to roughly 2,500 tokens. If a hook returns more, ZeroCode saves the full text under <temp_dir>/hook_outputs/<session_id>/<uuid>.txt and gives the model a head-and-tail preview with the saved-file path. This behavior is called spilling: ZeroCode stores oversized output on disk and replaces it with a shorter, model-visible preview. If the file can’t be written, the model still receives a truncated preview. Keep hook and plugin context concise. Context from multiple hooks and plugins adds up and can degrade model performance. Raising additionalContextLimit increases that risk. Avoid setting the limit to 0 unless the hook enforces a strict output cap; otherwise, a single hook can consume the entire context window. For any command hook that returns additionalContext, set additionalContextLimit on the handler to customize the approximate token threshold:
Omit additionalContextLimit to use the default 2500-token threshold. Use a positive integer to select a different threshold, or 0 to pass the handler’s complete additional context directly to the model. ZeroCode evaluates each matching handler independently. For events that can’t produce additional context, ZeroCode ignores additionalContextLimit and reports a configuration warning. The setting applies only to additionalContext. Tool feedback and continuation prompts keep the default limit. Because oversized output can be written to disk, avoid returning secrets or other sensitive data in hook output.

Hooks

SessionStart

matcher is applied to source for this event. Fields in addition to Common input fields: Plain text on stdout is added as extra developer context. JSON on stdout supports Common output fields and this hook-specific shape:
That additionalContext text is added as extra developer context. After ZeroCode compacts a root session, SessionStart hooks that match source: "compact" run before the next model request. This also applies when automatic compaction happens in the middle of a turn: ZeroCode delivers the hook’s additional context to the immediate continuation instead of waiting for a later user turn. If the hook returns continue: false, ZeroCode ends the turn without sending another model request.

SessionEnd

SessionEnd lets you run a command when a session ends, such as saving final notes or cleaning up files. It runs for the main thread when you archive or delete a conversation that’s still open, when ZeroCode closes normally, or after a conversation has been idle and isn’t open in any connected client for 30 minutes. It won’t run for subagents. Switching away from a conversation or calling thread/unsubscribe doesn’t end the session right away, so it won’t immediately run SessionEnd. Your hook can still read the session transcript while it runs. matcher filters reason for this event. For now, reason is always other. You can omit matcher or use other to run on every SessionEnd event. Fields in addition to Common input fields: For example, a SessionEnd command receives:
SessionEnd hooks are advisory. Their output won’t steer ZeroCode or keep the thread open. If a command times out or exits with an error, ZeroCode reports it as a hook failure.

SubagentStart

matcher is applied to agent_type for this event. Fields in addition to Common input fields: Plain text on stdout is added as extra developer context for the subagent. JSON on stdout supports systemMessage and this hook-specific shape:
That additionalContext text is added as extra developer context for the subagent. continue: false is parsed for compatibility, but it doesn’t stop the subagent from starting.

PreToolUse

PreToolUse can intercept Bash, file edits performed through apply_patch, MCP tool calls, and other local function tools. See Tool coverage for the supported paths and exceptions. matcher is applied to tool_name and matcher aliases. For file edits through apply_patch, matcher values can use apply_patch, Edit, or Write; hook input still reports tool_name: "apply_patch". Fields in addition to Common input fields: Plain text on stdout is ignored. JSON on stdout can use systemMessage. To deny a supported tool call, return this hook-specific shape:
ZeroCode also accepts this older block shape:
You can also use exit code 2 and write the blocking reason to stderr. To add model-visible context without blocking, return hookSpecificOutput.additionalContext:
To rewrite a supported tool call without blocking, return permissionDecision: "allow" with updatedInput:
For Bash commands and apply_patch, updatedInput must include a string command field. For MCP and other local function tools, updatedInput is the replacement arguments object. Return updatedInput only with permissionDecision: "allow"; other updatedInput shapes are reported as errors. permissionDecision: "ask", legacy decision: "approve", continue: false, stopReason, and suppressOutput are parsed but not supported yet. ZeroCode marks the hook run as failed, reports the error, and continues the tool call.

PermissionRequest

PermissionRequest runs when ZeroCode is about to ask for approval, such as a shell escalation or managed-network approval. It can allow the request, deny the request, or decline to decide and let the normal approval prompt continue. It doesn’t run for commands that don’t need approval. matcher is applied to tool_name and matcher aliases. Current canonical values include Bash, apply_patch, and MCP tool names such as mcp__server__tool; apply_patch also matches Edit and Write. Fields in addition to Common input fields: Plain text on stdout is ignored. Some tool inputs may include a human-readable description, but don’t rely on a tool_input.description field for every tool. To approve the request, return:
To deny the request, return:
If multiple matching hooks return decisions, any deny wins. Otherwise, an allow lets the request proceed without surfacing the approval prompt. If no matching hook decides, ZeroCode uses the normal approval flow. Don’t return updatedInput, updatedPermissions, or interrupt for PermissionRequest; those fields are reserved for future behavior and fail closed today.

PostToolUse

PostToolUse runs after supported tools produce output, including Bash, apply_patch, MCP tool calls, and other local function tools. For Bash, it also runs after commands that exit with a non-zero status. It can’t undo side effects from a tool that already ran. See Tool coverage for the supported paths and exceptions. matcher is applied to tool_name and matcher aliases. For file edits through apply_patch, matcher values can use apply_patch, Edit, or Write; hook input still reports tool_name: "apply_patch". Fields in addition to Common input fields: Plain text on stdout is ignored. JSON on stdout can use systemMessage and this hook-specific shape:
That additionalContext text is added as extra developer context. For this event, decision: "block" doesn’t undo the completed Bash command. Instead, ZeroCode records the feedback, replaces the tool result with that feedback, and continues the model from the hook-provided message. You can also use exit code 2 and write the feedback reason to stderr. To stop normal processing of the original tool result after the command has already run, return continue: false. ZeroCode will replace the tool result with your feedback or stop text and continue from there. updatedMCPToolOutput and suppressOutput are parsed but not supported yet. ZeroCode marks the hook run as failed, reports the error, and continues normal processing of the tool result.

Tool calls from code mode

When a model uses code mode to call a tool from JavaScript, hook decisions apply to that nested call. PreToolUse can stop the tool before it runs or rewrite its input. A blocking PostToolUse can’t undo the tool’s side effects, but it can keep the original result from reaching the running script.

PreCompact

PreCompact runs before ZeroCode compacts the chat. matcher is applied to trigger, whose values are manual and auto. Fields in addition to Common input fields: Plain text on stdout is ignored. JSON on stdout supports Common output fields. If a matching PreCompact hook returns continue: false, ZeroCode stops before compacting.

PostCompact

PostCompact runs after ZeroCode compacts the chat. matcher is applied to trigger, whose values are manual and auto. Fields in addition to Common input fields: Plain text on stdout is ignored. JSON on stdout supports Common output fields. If a matching PostCompact hook returns continue: false, ZeroCode stops after compacting.

UserPromptSubmit

matcher isn’t currently used for this event. Fields in addition to Common input fields: Plain text on stdout is added as extra developer context. JSON on stdout supports Common output fields and this hook-specific shape:
That additionalContext text is added as extra developer context. To block the prompt, return:
You can also use exit code 2 and write the blocking reason to stderr.

SubagentStop

matcher is applied to agent_type for this event. Fields in addition to Common input fields: SubagentStop expects JSON on stdout when it exits 0. Plain text output is invalid for this event. JSON on stdout supports Common output fields. To ask ZeroCode to continue the subagent flow, return:
You can also use exit code 2 and write the continuation reason to stderr. If any matching SubagentStop hook returns continue: false, that takes precedence over continuation decisions from other matching SubagentStop hooks.

Stop

matcher isn’t currently used for this event. Fields in addition to Common input fields: Stop expects JSON on stdout when it exits 0. Plain text output is invalid for this event. JSON on stdout supports Common output fields. To keep ZeroCode going, return:
You can also use exit code 2 and write the continuation reason to stderr. For this event, decision: "block" doesn’t reject the turn. Instead, it tells ZeroCode to continue and automatically creates a new continuation prompt that acts as a new user prompt, using your reason as that prompt text. If any matching Stop hook returns continue: false, that takes precedence over continuation decisions from other matching Stop hooks.

Schemas

The linked main branch schemas may include hook fields that are not in the current release. Use this page as the release behavior reference. If you need the exact current wire format, see the generated schemas in the ZeroCode GitHub repository.

Plain-text aliases

  • string | null