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:formatgo:fixgo: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.


Back to top

Copyright © 2026 idelchi. Distributed under the MIT License.