Docs › Extending AstraCode

Hooks

Run your own script when the agent is about to do something, and let it approve, block or rewrite the action.

A hook is your script, run at a specific moment in an agent run, given the event as JSON on stdin. It can let the action through, block it, or change it.

This is the escape hatch for anything AstraCode's own settings do not cover: enforce a house rule, refuse a command shape, log every edit somewhere, run a formatter at exactly the moment you want it.

The events

EventClaude Code's nameWhen it fires
sessionStartSessionStartA conversation begins: a chat's first message, a CLI run, a conversation's first request. Its context goes with that request.
beforeSubmitPromptUserPromptSubmitBefore your message is sent. May refuse or rewrite it.
preToolUsePreToolUseBefore any tool runs. May allow, refuse, ask, or rewrite its input.
beforeShellExecutionBefore a shell command specifically.
postToolUsePostToolUseAfter a tool returns. May add context to what the agent reads.
afterFileEditAfter a file is changed.
stopStopA run ends. May submit a follow-up, up to the hook's `loop_limit` in a row (5 by default, `null` for no limit).
subagentStartSubagentStartA subagent starts. Observation only.
subagentStopSubagentStopA subagent ends. Observation only.

Where hooks.json lives

Yours: ~/.astracode/hooks.json and ~/.cursor/hooks.json. The project's: .astracode/hooks.json and .cursor/hooks.json in every folder of the workspace, each run from its own folder. Hooks inside an agent file apply to that agent's tool calls only.

A project's hooks run only in a trusted workspace. A hook is a shell command, so a repository you have not trusted never gets to run one. In the CLI a -p run trusts its folder, as Claude Code's does; a conversation, acp or review needs --trust, ASTRACODE_TRUST=1, or a folder you trusted once.

How one is defined

Each hook names its event, a command, and optionally a timeout in seconds (30 by default), a matcher (a regular expression over the tool name, ours, Cursor's or Claude Code's: Shell, Bash, Read), and failClosed. The command runs as sh -c from its folder, given the event as JSON on stdin. Cursor's file shape and Claude Code's (PascalCase events, a matcher with its own hooks list) are both read as they are.

At the timeout the hook and everything it started are killed. Its stdout and stderr are kept to 64 KB each, and the context it adds to a tool result to 8,000 characters.

What a hook can say back

Exit 0 and JSON on stdout: permission (allow, deny or ask), user_message for you, agent_message for the agent, updated_input to replace the tool's input (or prompt for beforeSubmitPrompt), additional_context for the agent, followup_message from a stop hook. Exit 0 with nothing, or with prose, says nothing.

Exit 2 blocks, with the reason taken from stderr (or stdout when stderr is empty). On a stop hook it asks the agent to keep going with that reason; after a tool, the reason is passed to the agent.

Claude Code's answers are honoured too: hookSpecificOutput.permissionDecision with its permissionDecisionReason, hookSpecificOutput.updatedInput and additionalContext, decision: "block" with reason, decision: "approve", continue: false with stopReason, and systemMessage. A hook written for Claude Code is also given Claude Code's payload: PreToolUse, Bash, an absolute tool_input.file_path, tool_response.

A blocking hook is a real veto: beforeShellExecution returning a block stops the command, whatever the approval mode says.

Any other failure, a crash or a timeout, fails open: the action goes ahead and the failure is logged. Set "failClosed": true on a hook that guards something, and a failure denies instead.

Seeing what they did

The Customize page shows a log of recent hook runs, and the full output goes to an output channel. A hook that fails quietly is a hook you will spend an afternoon on.

If you have no hooks.json, none of this runs and it costs nothing.

All documentation

Get started

Agent

Context

Workspaces

Connectors

Extending AstraCode

CLI

Reference