Browse documentation

Technical documentation / Reference

Tool-call bash hooks

Feature docs index · Repository README

Trusted local hooks can audit, enforce policy, send notifications, or inject context. They are disabled by default and run in the active agent cwd, including child cwd for inherited subagent hooks.

Hooks wrap built-in tools such as read, bash/shell, hash_edit, write, grep, find, subagents, and web. Message hooks can also run after complete assistant messages or reasoning summaries. Hooks are separate from the bash tool and do not recurse.

Each hook receives JSON on stdin. Stdout and stderr remain local and cannot rewrite tool arguments or results. Provider context injection requires a successful after hook with explicit enablement. Hook processes are bounded and cleaned up after success; do not start background jobs that must outlive them.

Quick start

Add this to ~/.magi-code/settings.json under automation.hooks:

{
  "automation": { "hooks": {
    "enabled": true,
    "show_in_tui": true,
    "injected_content": {
      "show_in_transcript": false,
      "show_in_activity_tree": false,
      "style": "content"
    },
    "payload": "redacted",
    "failure_policy": "warn",
    "before_tool": [
      {
        "label": "audit-before",
        "command": "./scripts/magi-hooks/audit-before.sh"
      }
    ],
    "after_tool": [
      {
        "label": "audit-after",
        "command": "./scripts/magi-hooks/audit-after.sh",
        "failure_policy": "ignore"
      }
    ]
  }
  }}

Create the configured scripts in the launch repository. Both can use this body:

#!/usr/bin/env bash
set -euo pipefail
mkdir -p .magi-hooks
cat >> .magi-hooks/tool-hooks.jsonl
printf '\n' >> .magi-hooks/tool-hooks.jsonl

Make them executable. Prefer reviewed script paths to inline shell; configured commands must pass the preflight below. Audit files may contain sensitive local data, especially with full payloads.

Configuration schema

Global settings under automation.hooks:

FieldTypeDefaultBounds / valuesMeaning
enabledbooleanfalsetrue or falseMaster switch. false makes hooks inert.
show_in_tuibooleanfalsetrue or falseDisplay-only Mission Control opt-in. Shows matching hook start/success/failure activity rows when hooks are enabled; does not enable hook execution. Does not control provider context injection visibility.
injected_content.show_in_transcriptbooleanfalsetrue or falseDisplay provider context injection rows in Mission Control transcript. Visibility only; does not enable provider context injection.
injected_content.show_in_activity_treebooleanfalsetrue or falseDisplay provider context injection rows in Mission Control activity tree under the related tool/message activity when available, or as root rows otherwise. Visibility only; does not enable provider context injection.
injected_content.stylestring"content""content", "metadata"Transcript display style for injected content. content shows redacted/truncated injected content when the corresponding display setting is enabled; metadata hides injected content and shows only label, status, item count, and byte count.
payloadstring"redacted""redacted", "full"Default payload mode for hooks.
timeout_secondsinteger51 to 60Per-hook process timeout. Timeout is hook failure.
stdout_max_bytesinteger81921 to 65536Max captured stdout bytes. Exceeding cap is hook failure; output is suppressed in diagnostics.
stderr_max_bytesinteger81921 to 65536Max captured stderr bytes. Exceeding cap is hook failure; output is suppressed in diagnostics.
provider_context_injectionbooleanfalsetrue or falseAllow successful after_tool, after_assistant, and after_reasoning hooks to parse stdout context_items as provider-visible user messages.
provider_context_max_bytesinteger40961 to 16384Max total UTF-8 bytes of injected context content per hook.
failure_policystring"warn""ignore", "warn", "block", "fail"Default handling for hook failures. block is valid only for before_tool.
before_toolarray[]hook definitionsHooks run before matching tool dispatch.
after_toolarray[]hook definitionsHooks run after matching tool dispatch. failure_policy: "block" is rejected here.
after_assistantarray[]hook definitionsHooks run after the assistant produces a complete message. failure_policy: "block" is rejected. include_tools/exclude_tools do not apply.
after_reasoningarray[]hook definitionsHooks run after a complete reasoning summary. failure_policy: "block" is rejected. include_tools/exclude_tools do not apply.

Per-hook fields apply to all four phase arrays:

FieldTypeDefaultMeaning
labelstring"hook"Local diagnostic label. Empty labels become "hook".
commandstringrequiredShell command to run after safety preflight.
payloadstringglobal automation.hooks.payloadOverride payload mode for this hook.
failure_policystringglobal automation.hooks.failure_policyOverride failure policy for this hook. block only works in before_tool.
timeout_secondsintegerglobal automation.hooks.timeout_secondsOverride timeout, 1 to 60.
stdout_max_bytesintegerglobal automation.hooks.stdout_max_bytesOverride stdout cap, 1 to 65536.
stderr_max_bytesintegerglobal automation.hooks.stderr_max_bytesOverride stderr cap, 1 to 65536.
provider_context_injectionbooleanglobal automation.hooks.provider_context_injectionPer-hook context-injection override for after phases. No runtime effect for before_tool.
provider_context_max_bytesintegerglobal automation.hooks.provider_context_max_bytesPer-hook context byte limit, 1 to 16384.
include_toolsstring array[]If non-empty, hook runs only for exact tool names listed. Prefer canonical names such as read, bash, hash_edit, write, grep, find, subagents, web. Has no effect for after_assistant / after_reasoning.
exclude_toolsstring array[]Hook does not run for exact tool names listed. Applied after include_tools. Has no effect for after_assistant / after_reasoning.

Hook stdin JSON has a hard 1 MiB cap. Redacted payloads are summarized before spawn. When possible, trusted payload: "full" bodies above this cap move to an ephemeral local payload_ref file. Otherwise stdin remains valid JSON with payload.status: "omitted_too_large" and bounded summaries.

A hook that exits or stops reading stdin early may cause BrokenPipe during payload-writer cleanup. This is normal and does not affect classification. Exit status, timeout, cleanup warnings, and output limits still determine the result. Only other stdin write errors or envelope failures count as stdin failures.

Use include_tools: ["bash", "write", "hash_edit"] to select tools or exclude_tools: ["read", "grep"] to skip them. Matching is exact, and exclusions win. For a before-only policy gate, set failure_policy: "block" on that hook instead of globally.

Lifecycle

For each provider-requested built-in tool call:

  1. magi-code records the tool call in the session.
  2. Matching before_tool hooks run in array order. Each matching hook writes sanitized local-only hook_lifecycle started and terminal (success or failed) records when sessions are enabled.
  3. If all before hooks continue, the target tool runs.
  4. magi-code records the tool result in the session.
  5. Matching after_tool hooks run in array order with both request and result payload fields. Each matching hook writes sanitized hook_lifecycle records after the tool result. If provider context injection is enabled for a successful after hook, magi-code parses stdout JSON and appends validated user-role context items after the tool result for the next provider request.
  6. Hook warnings are displayed locally and recorded as local-only hook_diagnostic session events.
  7. Provider continuation receives the normal tool result, or a sanitized blocked-tool result if a before hook blocked the tool.

Successful session JSONL order is tool_call → before-hook hook_lifecycle started/terminal → tool_result → after-hook hook_lifecycle started/terminal → optional local-only hook_context_injection audit → optional provider-visible provider_context_item. Lifecycle and audit persistence are best-effort local telemetry, never provider-visible output. Inherited subagent records go to child session JSONL, not parent JSONL.

For message-phase hooks:

hook_lifecycle uses an allowlist: phase, label, target_tool, status, policy, category, target_ran, tool_call_id, activity_id, hook_index, elapsed_ms, and message. It never stores hook command text, stdin JSON, stdout, stderr, tool arguments/results, environment values, credentials, auth codes, bearer headers, or account ids.

The local-only hook_context_injection audit also uses an allowlist: schema_version, phase, label, target_tool, status, item_count, byte_count, max_bytes, and hook_index. It never stores raw stdout/stderr, hook stdin, tool arguments/results, or injected content.

Hook failure categories are exit, timeout, runner, stdin, and output-limit.

Mission Control visibility

All display settings default off and do not enable hook execution or context injection:

hook_lifecycle events persist even with live rows hidden. Resumed history shows recorded hooks without rerunning commands; injection history follows its display settings.

Payloads

Every hook receives one JSON object on stdin using schema magi-code.tool_hook with schema_version: 1. Top-level fields include phase, hook, tool, cwd, payload_mode, request, and, for after hooks, result.

Before payload shape:

{
  "schema": "magi-code.tool_hook",
  "schema_version": 1,
  "phase": "before_tool",
  "hook": {
    "label": "audit-before",
    "index": 0,
    "failure_policy": "warn",
    "payload_mode": "redacted"
  },
  "tool": { "name": "bash", "call_id": "call_abc123" },
  "context": {
    "cwd": "/Users/example/project",
    "session_id": "session-id-if-enabled",
    "session_path": "/Users/example/.magi-code/sessions/session-id-if-enabled.jsonl",
    "provider_id": "openai-codex",
    "model_id": "gpt-5",
    "agent_id": "tars",
    "invocation_mode": "mission_control",
    "turn_id": "turn-0",
    "message_id": "tool-tool-call-0",
    "subagent": false,
    "timestamp": "2026-05-26T00:00:00Z"
  },
  "affected_paths": [],
  "payload": {
    "inline": true,
    "status": "inline",
    "data": {
      "request": {
        "command": { "redacted": true, "kind": "command", "bytes": 42 },
        "timeout": 30
      }
    }
  },
  "payload_ref": null,
  "cwd": "/Users/example/project",
  "payload_mode": "redacted",
  "request": {
    "command": { "redacted": true, "kind": "command", "bytes": 42 },
    "timeout": 30
  }
}

After-tool payloads use the same envelope with phase: "after_tool" and a result in both payload.data and the top level. Results contain tool_name, success, content, and metadata; redacted content is "<redacted:tool-output>". A write target can appear as { "path": "notes.txt", "kind": "write_target", "source": "request.path" } in affected_paths.

Context metadata availability:

FieldSource / null behavior
session_id, session_pathActive session when persistence is enabled; otherwise null. Subagents use child session values.
provider_id, model_idRuntime provider selection; no credentials or account ids.
agent_idSelected primary-agent id or subagent task identity/label when available; otherwise null.
invocation_modeOne of print, mission_control, subagent. Set by caller path, not inferred from prompt text. The internal print label is not a CLI conversation mode.
turn_id, message_idLocal run-generated correlation ids. Before/after hooks for one tool call share message_id.
subagenttrue only for child subagent runs.
timestampHook payload construction time in UTC.

affected_paths is a bounded allowlisted array, not a comprehensive inventory of effects. read.paths uses kind: "read_target" and source: "request.paths". grep/ffgrep, find/fffind, and list_files use their path field with kind: "read_target" and source: "request.path"; write.path uses kind: "write_target" and source: "request.path". Each tool's capabilities.tools.<tool>.absolute_paths policy controls absolute paths outside cwd (aliases use the canonical tool's policy). Relative and symlink-resolved escapes forbidden by that policy, overlong paths, and paths that cannot be safely resolved are omitted. Bash command text, stdout/stderr, model prose, and arbitrary strings are never parsed for paths.

payload: "redacted" summarizes or removes sensitive material before the hook sees it:

payload: "full" sends original tool request JSON and full tool result JSON to the hook process. Use it only for trusted local scripts because it can expose command bodies, file contents, tool output, and secrets already present in tool inputs/results.

Large full payload behavior:

Provider context injection

Provider context injection is disabled by default. Enable it globally or per after_tool / after_assistant / after_reasoning hook when a trusted local script should add bounded context to the next provider request:

{
  "automation": {
    "hooks": {
      "enabled": true,
      "provider_context_injection": false,
      "provider_context_max_bytes": 4096,
      "after_tool": [
        {
          "label": "memory-after",
          "command": "./scripts/magi-hooks/memory-after.sh",
          "provider_context_injection": true,
          "provider_context_max_bytes": 4096
        }
      ]
    }
  }
}

Successful enabled after_tool hooks may print exactly this JSON shape on stdout:

{
  "context_items": [
    { "role": "user", "content": "Relevant bounded memory for next provider call." }
  ]
}

Rules:

after_assistant and after_reasoning phases

Configure these arrays like after_tool; tool filters do not apply. Payloads use tool.name: "assistant" or "reasoning", with call ids such as assistant-turn-0 or reasoning-turn-0. request.text holds the full completed message or reasoning summary, including in payload.data.request. affected_paths is empty and result is absent/null. Both use the same context_items stdout contract.

Failure policies

PolicyBefore hook failureAfter hook failureafter_assistant / after_reasoning failure
ignoreContinue silently.Continue silently.Continue silently.
warnContinue and show sanitized local warning. Default.Continue and show sanitized local warning. Default.Continue and show sanitized local warning. Default.
blockPrevent target tool. Assistant receives sanitized blocked result.Not allowed. after_tool cannot block because target already ran; config using it is rejected.Not allowed. Same as after_tool; config using it is rejected.
failStop the turn before target tool runs.Preserve original tool result locally, then fail the turn before provider continuation.Fail the turn before provider continuation.

A hook failure means non-zero exit, timeout, runner/preflight error, stdin write error, or stdout/stderr exceeding configured capture limits.

Command safety and cwd

Hook commands run through the platform shell with cwd set to the agent cwd and a sanitized environment:

Before spawning, magi-code rejects hook command strings containing shell expansion characters that make cwd-scoping ambiguous:

$  ~  `  {  }

It also rejects cd, parent-directory path components such as .., and absolute paths when capabilities.tools.bash.absolute_paths is false. With the default capabilities.tools.bash.absolute_paths: true, absolute hook command paths may pass preflight, but script paths inside the repository are easier to review and move across machines.

Use a simple command such as ./scripts/magi-hooks/audit-jsonl.sh or python3 scripts/magi-hooks/audit.py. Executable scripts need a shebang and executable permission. Commands such as echo "$PWD", cd scripts && ./hook.sh, ../hooks/audit.sh, ~/hooks/audit.sh, and ./scripts/{audit,notify}.sh fail preflight.

Put complex shell or Python logic inside the reviewed script. Command-string restrictions do not restrict the script body and are not a sandbox. See Security notes for the exact environment profile.

Script examples

This Python audit records only phase and tool name rather than whole payloads:

#!/usr/bin/env python3
import json
import pathlib
import sys

payload = json.load(sys.stdin)
pathlib.Path(".magi-hooks").mkdir(exist_ok=True)
with open(".magi-hooks/python-hooks.jsonl", "a", encoding="utf-8") as handle:
    handle.write(json.dumps({"phase": payload["phase"], "tool": payload["tool"]["name"]}) + "\n")

For validators, opt in to payload: "full" only for scripts you trust. A before hook with failure_policy: "block" can reject a request by exiting nonzero. Handle payload_ref and omitted_too_large explicitly; a pattern check against a missing or summarized body is not a complete safety check.

Privacy and sessions

Troubleshooting hooks

SymptomLikely causeFix
Hook never runsautomation.hooks.enabled is false, arrays are empty, or filters do not match tool name.Enable hooks and check exact names in include_tools / exclude_tools.
Hooks run but no successful live rows appear in Mission Controlautomation.hooks.show_in_tui is false, Mission Control is not active, or no matching hook ran. Session hook_lifecycle records still persist when hooks run and sessions are enabled.Set "show_in_tui": true under automation.hooks for live rows, keep automation.hooks.enabled: true, and run magi-code.
Settings fail to load with block errorafter_tool, after_assistant, or after_reasoning hook inherits or sets failure_policy: "block".Set that after hook to ignore, warn, or fail.
Command rejected before spawnCommand string contains $, ~, backticks, {}, cd, .., or disallowed absolute path.Move logic into a script and configure a simple script path.
Hook times outScript exceeds timeout_seconds or waits for interactive input.Read stdin once, avoid prompts, increase timeout up to 60.
Hook fails with stdinNon-BrokenPipe write error or envelope failure. Closing stdin early with BrokenPipe alone is ignored.Read stdin once; inspect payload.status, payload_ref, and sanitized diagnostics. Large full payloads normally spill or use omitted_too_large.
Hook fails with output-limitScript printed more than stdout/stderr caps.Write large logs to files; keep stdout/stderr short; raise cap up to 65536.
Script cannot find filesHooks run in the active agent cwd. Inherited subagent hooks run in the child task cwd.Use paths relative to the task cwd, or reviewed absolute paths when policy permits.
Script sees redacted contentDefault payload mode is redacted.Use per-hook payload: "full" only for trusted validators.
Assistant did not see hook warningHook diagnostics are local-only.Inspect terminal/TUI transcript or session JSONL; use block/fail policy when provider-visible behavior must change.

Edit this page on GitHub ↗