Skip to content

Hooks

Hooks run scripts in response to lifecycle events (a session starts, a tool is about to run, a model response just landed, …). aix lets you author one set of hooks in ai.json and translates each one into the native format of every target editor.

Add a hook from the command line:

Terminal window
aix add hook pre_command --command "npm run lint"
aix add hook pre_file_write --matcher "Write|Edit" --command ./scripts/guard.sh
aix add hook ./hooks/guard.json

aix add hook writes the hook to ai.json and installs it, joining whatever is already registered for that event. aix list hooks shows what is configured, aix list --all shows what your editors actually hold, and aix remove hook <event> takes one back out of ai.json and your editor config. See aix add and aix remove for every flag.

Or define hooks in ai.json directly:

{
"hooks": {
"session_start": [
{
"matcher": ".*",
"hooks": [{ "command": "./scripts/init-session.sh", "show_output": true }]
}
],
"pre_tool_use": [
{
"matcher": "Bash",
"hooks": [{ "command": "./scripts/audit-bash.sh" }]
}
]
}
}

When you run aix install, each enabled editor adapter translates that block into the editor’s native config format and writes it to the right file. Nothing is dropped silently: aix warns when the editor has no hooks support at all, when an individual event has no native equivalent, when an action field cannot be expressed, and when the editor has no hooks file at the scope you targeted.

To install one hook fragment without creating ai.json, use direct install:

Terminal window
aix install ./hooks/pre-command.jsonc --type hook --target claude-code --user

In editors with native plugin support (such as Claude Code), hooks defined in ai.json and hooks bundled within enabled plugins both trigger on matching lifecycle events. Claude Code executes matching hooks in sequence (both project/user hooks and active plugin hooks run for the given event).

aix defines a normalized event vocabulary covering every event surfaced by the supported editors. Adapters map each to the editor’s native equivalent.

  • session_start, session_end
  • setup (Claude Code’s --init-only / --init / --maintenance)
  • pre_prompt — before submitting the user’s prompt
  • user_prompt_expansion — when a slash command expands into its body
  • pre_tool_use, post_tool_use, post_tool_use_failure
  • post_tool_batch — after a parallel tool batch finishes
  • pre_tool_selection — before the model decides which tools it can call
  • permission_request, permission_denied
  • pre_file_read, post_file_read
  • pre_file_write, post_file_write
  • pre_command, post_command
  • pre_mcp_tool, post_mcp_tool
  • pre_tab_file_read, post_tab_file_edit (Cursor Tab)
  • pre_model_request — before sending a request to the LLM
  • post_model_response — when the LLM emits a response
  • pre_response_chunk — for every streaming chunk
  • pre_agent, post_agent
  • post_response, post_response_with_transcript
  • agent_stop, subagent_start, subagent_stop, subagent_idle
  • pre_compact, post_compact
  • task_created, task_completed
  • worktree_setup, worktree_remove
  • instructions_loaded, config_change, cwd_changed, directory_added, file_changed
  • notification, elicitation, elicitation_result, error_occurred

Every action under hooks[].hooks[] accepts an optional set of fields. Adapters use whatever the target editor surfaces and report the rest via install-time warnings.

FieldPurpose
typecommand (default), http, mcp_tool, prompt, or agent.
commandShell command to run.
bash / powershellCross-platform commands. Used directly by Copilot and Windsurf; Codex writes powershell as commandWindows; Claude Code expands to two entries with shell selectors.
shellbash or powershell selector when only one of the command fields is set.
cwd / working_directoryWorking directory for the command.
envEnvironment variables to pass through (Copilot only today).
timeoutTimeout in seconds. Adapters convert (Copilot uses timeoutSec).
async / async_rewakeBackground execution. async also reaches Codex, which added it in 0.148.0; async_rewake is Claude Code only.
ifPermission-rule guard expression (Claude Code).
status_messageCustom spinner message (Claude Code).
onceRun once per session (Claude Code skill / agent frontmatter).
url, headers, allowed_env_varsHTTP webhook fields (Claude Code http).
mcp_server, mcp_tool, mcp_inputMCP tool dispatch (Claude Code mcp_tool).
prompt, modelLLM-evaluated prompt or agent (Claude Code, Cursor, Copilot sessionStart).
description, nameDocumentation / log identifier.
fail_closedBlock the action when the hook itself fails (Cursor).
loop_limitMax auto-triggered follow-ups (Cursor).
show_outputSurface output in the editor UI (Windsurf).
Event familyClaude CodeCursorCopilotWindsurfAntigravity
session_start / session_endyesyesyes—yes
setupyes————
user_prompt_expansionyes————
pre_promptyesyesyesyes—
pre_tool_use / post_tool_useyesyesyesmaps to read/write/command/mcp variantsyes
post_tool_use_failureyesyesyes——
permission_request / permission_deniedyespermission_request via preToolUseyes (request)——
pre_tool_selection————yes
pre_model_request / post_model_response / pre_response_chunk—partial (afterAgentResponse, afterAgentThought)——yes
pre_agent / post_agent————yes
post_response / post_response_with_transcript—partial—yes (with transcript variant)—
agent_stop / subagent_*yesyesyesagent_stop only—
pre_compact / post_compactyesyes (pre only)yes (pre only)—yes (PreCompress)
Tab hooks—yes———
notification / elicitation*yes—yes (notification)—yes (Notification)
error_occurred——yes——
Worktree / fs / config eventsyes——post_setup_worktree only—

aix only writes the hook configuration. Whether a hook blocks an action is a contract between your script and the editor. The shapes scripts must print to stdout (or signal via exit code 2) are summarized below.

{
"decision": "block",
"reason": "Why blocked",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask|defer",
"permissionDecisionReason": "Reason",
"updatedInput": { "field": "value" }
}
}

Exit code 2 also blocks the matching event.

{
"permission": "allow|deny|ask",
"user_message": "shown to the user",
"agent_message": "shown to the agent",
"updated_input": { "field": "value" }
}

Cursor also accepts failClosed: true on the hook config to block on hook errors.

{
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "Required for deny",
"modifiedArgs": { "field": "value" }
}

agentStop and subagentStop use { "decision": "block|allow", "reason": "..." }.

Pre-hooks block the action with exit code 2 and use stderr as the rejection reason. Post-hooks cannot block.

{
"decision": "deny|allow",
"reason": "Sent back to the agent on deny"
}
EditorProjectUser
Claude Code.claude/settings.json~/.claude/settings.json
Cursor.cursor/hooks.json (with version: 1)~/.cursor/hooks.json
Copilot.github/hooks/hooks.json~/.config/github-copilot/hooks/hooks.json
Windsurf.windsurf/hooks.json~/.codeium/windsurf/hooks.json
Antigravity.agents/hooks.json~/.gemini/config/hooks.json
Codex.codex/hooks.json~/.codex/hooks.json
Grok.grok/hooks.json~/.grok/hooks.json

Codex also reads hooks from inline [hooks] tables in config.toml. aix writes the JSON files instead, so it never has to rewrite the TOML file that holds your MCP servers.

Zed and OpenCode do not currently support hooks. aix warns when you target them with a hook config.