Hooks

Hooks are user-configurable shell commands that run automatically before or after LLM tool execution, enabling formatting, linting, and validation without LLM involvement. Defined in .aura/config/hooks/*.yaml as a YAML map of hook names to definitions.

Hook Fields

Field Type Default Description
description string   Summary shown in system prompts via .Hooks.Active template data
event string   "pre" (before Execute, can block) or "post" (after Execute)
matcher string   Regex matched against tool name. Empty = match all tools
files string   Glob matched against file basenames. Hook skipped if no match
command string   Shell command executed via mvdan/sh. Receives JSON on stdin
timeout int 10 Seconds before the hook is killed
depends []string [] Hook names (same event) that must run before this hook
inherit []string [] Hook names to inherit fields from — explicitly set fields override
silent bool false Discard all output and exit codes
disabled *bool false Skip this hook. Pointer semantics: absent = inherit from parent, false = enabled, true = disabled

JSON Stdin

Every hook receives a JSON object on stdin. hook_event is "PreToolUse" or "PostToolUse". tool.output is only present in post hooks. file_paths is extracted from tool input and may be empty.

{
  "hook_event": "PreToolUse",
  "tool": {
    "name": "Write",
    "input": { "path": "main.go", "content": "..." },
    "output": "..."
  },
  "cwd": "/home/user/project",
  "file_paths": ["/home/user/project/main.go"]
}

$FILE and Exit Codes

When file paths are present, $FILE is set to a space-separated list of matched paths. Use it in commands: gofmt -w $FILE.

Exit Code Semantics

Exit code Pre hook Post hook
0 Parse stdout for JSON Parse stdout for JSON
2 Block tool execution Report status and output
other Report status and output Report status and output

JSON Stdout (exit 0)

On exit 0, a hook may optionally print JSON to stdout:

{
  "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)
reason string Reason shown when deny is true

Non-JSON stdout is treated as a plain-text message.

DAG Ordering via depends

Hooks within the same event run in topological order; unrelated hooks run alphabetically. Cycles and missing dependencies are hard errors at config load.

go:lint:
  depends: [go:fix] # runs after go:fix completes

Config Inheritance via inherit

go:fix:
  inherit: [go:format] # copies event, matcher, files, timeout, silent from go:format
  depends: [go:format]
  command: |
    for f in ${FILE}; do
      gopls codeaction -kind=source.organizeImports -exec -write ${f}
    done

Per-Agent and Per-Mode Filtering

Use hooks.enabled / hooks.disabled in agent or mode frontmatter:

---
name: MyAgent
hooks:
  disabled: ["go:*"] # disable all Go hooks for this agent
---

Patterns support * wildcards. Filter chain: agent → mode. Excluding a hook also prunes any hooks that depend on it. Use /hooks to see the filtered view for the current agent and mode.

Prompt Awareness

Active hooks are available as template data in system, agent, and mode prompts. The description field is what appears — hooks without one show their name and event only.

.Hooks.Display gives a pre-rendered summary; .Hooks.Active is a slice with fields Name, Description, Event, Matcher, Files, and Command.

{{- if .Hooks.Active }}
Hooks that run after your tool calls:
{{ range .Hooks.Active -}}
- {{ .Name }}: {{ .Description }} ({{ .Event }}, files: {{ .Files }})
{{ end -}}
{{- end }}

Example: Go Hook Chain

# .aura/config/hooks/go.yaml

go:format:
  description: Format Go files with golangci-lint after edits
  event: post
  matcher: "Patch|Write|Edit"
  files: "*.go"
  silent: false
  command: |
    jq -r '.file_paths[] | select(endswith(".go"))' |
    while IFS= read -r file; do
      [ -f "${file}" ] || continue
      golangci-lint fmt --diff "${file}"
      golangci-lint fmt "${file}" || exit
    done
  timeout: 15

go:fix:
  description: Apply available gopls fixes and organize imports after formatting
  inherit: [go:format]
  depends: [go:format]
  silent: false
  command: |
    jq -r '.file_paths[] | select(endswith(".go"))' |
    while IFS= read -r file; do
      [ -f "${file}" ] || continue
      for kind in source.fixAll source.organizeImports; do
        while true; do
          actions=$(gopls codeaction -kind="${kind}" "${file}") || exit
          [ -n "${actions}" ] || break
          gopls codeaction -kind="${kind}" -exec -write -diff "${file}" || exit
        done
      done
    done
  timeout: 30

Execution order: go:format → go:fix. Patch, Write, and Edit receive the same formatting and gopls assistance; LSP diagnostics remain available during editing. Deleted files are skipped. The hooks read JSON paths so filenames containing spaces remain intact. Gopls actions are listed before execution: no matching action is a normal no-op, while genuine errors remain visible. This example requires jq.

Run package-wide fixes, builds, tests, and lint checks after a coherent change and before completion, following the project’s verification instructions. Record this checkpoint in the shared .aura/AGENTS.md or the project’s AGENTS.md; these editing hooks do not perform full verification. Complete related edits before checking the package so temporary errors do not repeatedly trigger the full fix/lint chain. Update module dependencies after their imports have been added.

Use source.fixAll for automatic gopls fixes, alongside import organization. Gopls reserves this category for fixes intended to be safe to apply automatically. A blanket quickfix loop can delete functions and types that have not yet acquired callers during incremental editing. See the gopls quick-fix documentation.

Example: Pre-Hook Validation

# .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 as the rejection reason.

Message Injection (Injectors)

Injectors are plugins that fire at specific points in the assistant loop and inject messages into the conversation to guide LLM behavior. Unlike shell hooks (which run commands), injectors are Go plugins that produce structured feedback fed directly to the LLM.

Shipped Injectors

Plugin Timing Purpose
todo-reminder BeforeChat Reminds about pending todos every N iterations
max-steps BeforeChat Disables tools after the iteration limit is reached
session-stats BeforeChat Shows session stats summary (turns, tool calls, context usage, top tools) every 5 turns
todo-not-finished AfterResponse Warns if todos incomplete and no tool calls
empty-response AfterResponse Handles empty LLM responses
done-reminder AfterResponse Reminds LLM to call Done tool when stopping
loop-detection AfterToolExecution Detects repeated identical tool calls
failure-circuit-breaker AfterToolExecution Stops after 3+ consecutive different tool failures

Example injectors ship under .aura/plugins/injectors/ and are not installed by aura init. Copy them to your own .aura/plugins/ directory to activate them.

Conditions

Injectors use a condition expression in plugin.yaml evaluated before every hook call. The condition syntax is identical to /assert — see /assert. Set once: true to fire only on the first match.

Custom Injectors

To write a custom injector, add a Go plugin under .aura/plugins/ implementing one of the nine hook timing points (BeforeChat, AfterResponse, BeforeToolExecution, AfterToolExecution, OnError, BeforeCompaction, AfterCompaction, OnAgentSwitch, TransformMessages). See Plugins for the full authoring guide.


Back to top

Copyright © 2026 idelchi. Distributed under the MIT License.