LLM Hooks
LLM Hooks let you define policy enforcement rules as markdown files. Each hook contains a natural language prompt that an LLM evaluates at runtime to decide whether to approve, deny, or warn about an operation.
How it works
mermaidflowchart LR A[Event fires] --> B{Condition match?} B -->|no| C[Skip] B -->|yes| D{Cached?} D -->|yes| E[Return cached result] D -->|no| F[Call LLM] F --> G{Decision} G -->|approve| H[Allow] G -->|deny| I[Block] G -->|warn| J[Warn + allow]
Hook files
Hooks are markdown files with YAML frontmatter, stored in two directories:
| Directory | Scope |
|---|---|
.mayros/hooks/ | Project-level hooks |
~/.mayros/hooks/ | User-level hooks (global) |
Format
markdown--- name: "no-force-push" events: "before_tool_call" description: "Block force pushes to protected branches" condition: 'params.command.includes("push") && params.command.includes("--force")' model: "anthropic/claude-sonnet-4-20250514" timeout: 20000 cache: "global" priority: 150 enabled: "true" --- # No Force Push Policy Evaluate whether this git push command targets a protected branch. If the command uses --force and targets main, master, or production branches, deny the operation. Otherwise, approve it. Context: {{context}}
Frontmatter fields
| Field | Required | Default | Description |
|---|---|---|---|
name | yes | — | Unique hook name |
events | yes | — | Comma-separated event names |
description | no | — | Human-readable description |
condition | no | — | Safe expression (evaluated without eval) |
model | no | config default | LLM model override |
timeout | no | 15000 | Evaluation timeout (ms) |
cache | no | session | Cache scope: none, session, global |
priority | no | 100 | Higher = runs earlier |
enabled | no | true | Enable/disable hook |
Safe condition language
Conditions use a safe expression parser — no eval is used. The grammar supports:
expr → orExpr
orExpr → andExpr ( "||" andExpr )*
andExpr → notExpr ( "&&" notExpr )*
notExpr → "!" notExpr | comparison
comparison → primary ( ("==" | "!=") primary )?
primary → "true" | "false" | string | propertyChain | "(" expr ")"
Supported methods: .includes(arg), .startsWith(arg), .endsWith(arg)
Examples:
toolName == "exec"
params.command.includes("rm")
!sessionKey
toolName == "exec" && params.command.startsWith("git")
If a condition fails to parse, it defaults to true (hook always runs).
Supported events
| Event | Hook type | Handler output |
|---|---|---|
before_tool_call | Modifying | { block, blockReason } on deny |
after_tool_call | Void | — |
message_sending | Modifying | { cancel, cancelReason } or { modified } |
before_prompt_build | Modifying | { prependContext } on warn |
before_agent_start | Modifying | { prependContext } on deny |
session_start | Void | Clears caches |
session_end | Void | Clears caches |
When multiple hooks match an event, they run in priority order. The first deny result short-circuits — remaining hooks are skipped.
Caching
LLM evaluations are cached to avoid redundant calls:
| Scope | Behavior | Cleared |
|---|---|---|
none | Never cached | — |
session | Cached per session | On session_start / session_end |
global | Cached across sessions | TTL-based (default: 5 minutes) |
Cache keys are derived from the hook body hash + context hash (SHA256).
LLM response format
The LLM must return a JSON object:
json{ "decision": "approve", "reason": "The command only reads from a safe branch" }
Valid decisions: approve, deny, warn.
Configuration
json5{ llmHooks: { enabled: true, projectHooksDir: ".mayros/hooks", userHooksDir: "~/.mayros/hooks", defaultModel: "anthropic/claude-sonnet-4-20250514", defaultTimeoutMs: 15000, // 1000–120000 defaultCache: "session", // "none" | "session" | "global" maxConcurrentEvals: 3, // 1–10 globalCacheTtlMs: 300000, // >= 10000 (5 min default) } }
CLI
bashmayros llm-hooks list # Show all hooks (status, events, priority) mayros llm-hooks test hook-file.md # Dry-run hook (--tool, --params) mayros llm-hooks cache # Cache statistics mayros llm-hooks reload # Reload hooks from disk
Related
- Hooks — hook system overview
- Bash Sandbox — command-level sandboxing
- Interactive Permissions — intent-based permissions
- llm-hooks CLI — command reference