Hooks
Hooks are user-configurable shell commands that run automatically before or after LLM tool execution. They enable formatting, linting, validation, and other side effects without LLM involvement.
Config Location
.aura/config/hooks/*.yaml
Each file is a YAML map of hook names to definitions. Multiple hooks can live in one file, or each in its own file — the loader is decoupled from filenames.
Hook Fields
| Field | Type | Default | Description |
|---|---|---|---|
description | string | Human-readable summary. Exposed in system prompts via {{ .Hooks.Active " }} template data | |
event | string | "pre" (before Execute, can block) or "post" (after Execute) | |
matcher | string | Regex matched against the tool name. Empty = match all tools | |
files | string | Glob matched against file basenames (e.g. *.go). Hook skipped if no match | |
command | string | Shell command executed via mvdan/sh (pure-Go interpreter). Receives JSON on stdin | |
timeout | int | 10 | Seconds before the hook process is killed |
depends | []string | [] | Hook names (same event) that must run before this hook |
inherit | []string | [] | Hook names to inherit fields from — only explicitly set fields override |
silent | bool | false | Suppress all output and exit codes. Hook runs but never produces messages |
disabled | *bool | false | Skip this hook entirely. It still appears in /hooks but never executes. Uses pointer semantics: absent = inherit from parent (when using inherit:), explicit false = enabled, true = disabled |
JSON Stdin
Every hook receives a JSON object on stdin describing the triggering event:
{
"hook_event": "PreToolUse",
"tool": {
"name": "Write",
"input": { "path": "main.go", "content": "..." }
},
"cwd": "/home/user/project",
"file_paths": ["/home/user/project/main.go"]
}
Post hooks additionally receive tool.output:
{
"hook_event": "PostToolUse",
"tool": {
"name": "Write",
"input": { "path": "main.go", "content": "..." },
"output": "File written successfully."
},
"cwd": "/home/user/project",
"file_paths": ["/home/user/project/main.go"]
}
| Field | Present | Description |
|---|---|---|
hook_event | Always | "PreToolUse" or "PostToolUse" |
tool.name | Always | Name of the tool being called |
tool.input | Always | Tool arguments as an object |
tool.output | Post only | Tool result string |
cwd | Always | Working directory at the time of the call |
file_paths | Always | File paths extracted from tool input (may be empty) |
$FILE Environment Variable
When file paths are present, the $FILE environment variable is set to a space-separated list of matched file paths. Use it in commands:
go:format:
event: post
matcher: "Patch|Write"
files: "*.go"
command: gofmt -w $FILE
Exit Code Semantics
| Exit code | Pre hook behavior | Post hook behavior |
|---|---|---|
0 | Success — parse stdout for JSON | Success — parse stdout for JSON |
2 | Block tool execution | Append stderr as feedback |
| other | Append stderr as feedback | Append stderr as feedback |
Pre hooks returning exit 2 block the tool call entirely. The LLM receives the hook’s stderr (or a default message) as the tool result, and the underlying tool does not execute.
Post hooks never block — they can only append feedback to the tool output returned to the LLM. If a post-hook returns deny: true or exit code 2, the deny is silently ignored (the tool already executed). Run with --debug to see [hooks] post-hook <name> returned deny but post-hooks cannot block in the debug log.
JSON Stdout (exit 0)
On exit 0, a hook may optionally print JSON to stdout to influence the result:
{ "message": "Formatted successfully." }
{ "deny": true, "reason": "File contains forbidden pattern." }
| Field | Type | Description |
|---|---|---|
message | string | Text appended to tool output as feedback to the LLM |
deny | bool | true to block the tool (pre only) or add feedback |
reason | string | Reason shown when deny is true |
If stdout is not valid JSON, it is treated as a plain-text message.
DAG Ordering via depends
Hooks within the same event run in topological order determined by depends. Hooks with no dependency relationship run in alphabetical order. Cycles or missing dependencies are hard errors at config load.
go:lint:
depends: [go:fix] # runs after go:fix completes
Config Inheritance via inherit
A hook can inherit all fields from one or more parent hooks, overriding only what it sets explicitly:
go:fix:
inherit: [go:format] # copies event, matcher, files, timeout, silent from go:format
depends: [go:format]
command: golangci-lint run --fix $FILE
Fields defined directly on the hook take precedence over inherited values.
Silent Mode
silent: true runs the hook but discards all output and exit codes. Use it for fire-and-forget side effects (formatting, indexing) where you do not want feedback returned to the LLM:
go:format:
event: post
matcher: "Patch|Write"
files: "*.go"
silent: true
command: gofmt -w $FILE
Per-Agent and Per-Mode Filtering
By default, all hooks apply to every agent and mode. Use hooks.enabled / hooks.disabled in agent or mode frontmatter to scope hooks:
---
name: MyAgent
hooks:
disabled: ["go:*"] # disable all Go hooks for this agent
---
Patterns support * wildcards (same syntax as tool filtering). The filter chain is: agent → mode — mode patterns further restrict the agent-filtered set.
Cascade pruning: When a hook is excluded and other hooks depend on it via depends, the dependents are automatically removed. For example, disabling go:format also prunes go:fix (depends on go:format) and go:lint (depends on go:fix).
The /hooks command shows the filtered view for the current agent and mode.
Inspecting Hooks
Use /hooks in the interactive assistant to display all active hooks grouped by event in execution order:
/hooks
Prompt Awareness
Active hooks are available as template data in system, agent, and mode prompts. The default agentic prompt includes a conditional section that renders when hooks are configured, so the LLM knows to expect automated file modifications.
The description field is what appears in the prompt. Hooks without a description show their name and event only.
go:format:
description: Format Go files with gofmt after edits
event: post
matcher: "Patch|Write"
files: "*.go"
command: gofmt -w $FILE
Template data available:
| Field | Description |
|---|---|
.Hooks.Display | Pre-rendered summary of all active hooks |
.Hooks.Active | Slice of hook entries for fine-grained rendering |
.Hooks.Active[].Name | Hook name |
.Hooks.Active[].Description | Description text |
.Hooks.Active[].Event | "pre" or "post" |
.Hooks.Active[].Matcher | Tool regex |
.Hooks.Active[].Files | File glob |
.Hooks.Active[].Command | Shell command |
Custom prompt example:
{{- if .Hooks.Active }}
Hooks that run after your tool calls:
{{ range .Hooks.Active -}}
- {{ .Name }}: {{ .Description }} ({{ .Event }}, files: {{ .Files }})
{{ end -}}
{{- end }}
Example: Go Hook Chain
The following example chains three hooks with inheritance and dependency ordering. Format runs silently first, fix runs after it and applies quick-fixes, lint runs last and reports remaining issues.
# .aura/config/hooks/go.yaml
go:format:
event: post
matcher: "Patch|Write"
files: "*.go"
silent: true
command: golangci-lint fmt $FILE
timeout: 15
go:fix:
inherit: [go:format]
depends: [go:format]
command: |
golangci-lint run --fix $FILE
for f in $FILE; do
while gopls codeaction -kind=quickfix -exec -write $f 2>/dev/null; do :; done
gopls codeaction -kind=source.organizeImports -exec -write $f 2>/dev/null
done
timeout: 30
go:lint:
inherit: [go:format]
depends: [go:fix]
silent: false
command: golangci-lint run $FILE
Execution order for post hooks: go:format → go:fix → go:lint.
Example: Pre-Hook Validation
Pre hooks can reject tool calls before they execute:
# .aura/config/hooks/validate.yaml
validate-bash:
event: pre
matcher: "Bash"
command: .aura/hooks/validate-bash.sh
timeout: 5
If .aura/hooks/validate-bash.sh exits with code 2, the Bash tool call is blocked and the LLM receives the script’s stderr output as the rejection reason.