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.

Define hooks in ai.json:

{
"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. Unsupported events are warned about — never silently dropped.

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, 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; 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 (Gemini uses milliseconds, Copilot uses timeoutSec).
async / async_rewakeBackground execution (Claude Code).
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 CodeCursorCopilotWindsurfGemini
session_start / session_endyesyesyesyes
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_selectionyes
pre_model_request / post_model_response / pre_response_chunkpartial (afterAgentResponse, afterAgentThought)yes
pre_agent / post_agentyes
post_response / post_response_with_transcriptpartialyes (with transcript variant)
agent_stop / subagent_*yesyesyesagent_stop only
pre_compact / post_compactyesyes (pre only)yes (pre only)yes (PreCompress)
Tab hooksyes
notification / elicitation*yesyes (notification)yes (Notification)
error_occurredyes
Worktree / fs / config eventsyespost_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",
"hookSpecificOutput": {
"additionalContext": "Injected before the response",
"tool_input": { "field": "value" },
"tailToolCallRequest": { "name": "another_tool", "args": {} }
}
}
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
Gemini.gemini/settings.json (under hooks, merged with MCP)~/.gemini/settings.json

Editors not in the table (Codex, Zed, OpenCode) do not currently support hooks. aix warns when you target them with a hook config.