Hooks Configuration Reference
Complete reference for flint-hooks.toml configuration.
Overview
Hooks allow you to run scripts in response to runtime events. Configure hooks in flint-hooks.toml at your Flint root.
# flint-hooks.toml
[hooks.FileChanged](/hooks-filechanged)
matcher = "Mesh/Tasks/*.md"
command = "./hooks/on-task-change.ts"
timeout = 30
[hooks.StatusChanged](/hooks-statuschanged)
matcher = { from = "todo", to = "in-progress" }
command = "./hooks/on-task-start.sh"File Location
The runtime looks for flint-hooks.toml in the Flint root directory:
my-flint/
├── flint.toml
├── flint-hooks.toml # Hook configuration
├── hooks/ # Recommended: hook scripts here
│ ├── on-task-change.ts
│ └── notify-urgent.py
└── Mesh/Missing flint-hooks.toml is valid - the runtime runs with no hooks configured.
Configuration Format
Basic Structure
[hooks.<EventType>](/hooks-eventtype)
command = "<script-path>"
matcher = "<pattern-or-object>" # optional
timeout = <seconds> # optionalEvent Types
| Event Type | Description |
|---|---|
FileChanged | Markdown file in watched paths changed |
StatusChanged | Frontmatter status field changed |
TagAdded | Tag added to frontmatter |
TagRemoved | Tag removed from frontmatter |
AgentStart | Agent session started |
AgentComplete | Agent session completed |
ConnectionReceived | Connection event received |
RuntimeStart | Runtime started |
RuntimeStop | Runtime stopping |
Hook Fields
command (required)
The script to execute when the hook triggers.
[hooks.FileChanged](/hooks-filechanged)
command = "./hooks/on-change.ts"Path resolution:
- Relative paths resolve from Flint root
- Absolute paths used as-is
Script detection by extension:
| Extension | Execution |
|---|---|
.ts | tsx <file> |
.py | python3 <file> |
.sh | bash <file> |
| Other | Direct execution |
matcher (optional)
Filter which events trigger the hook. Format depends on event type.
String matcher (FileChanged only):
[hooks.FileChanged](/hooks-filechanged)
matcher = "Mesh/Tasks/*.md"
command = "./hooks/on-task.ts"Uses glob pattern matching via minimatch. Matches against event.payload.path.
Object matcher:
[hooks.StatusChanged](/hooks-statuschanged)
matcher = { from = "todo", to = "in-progress" }
command = "./hooks/on-start.ts"
[hooks.TagAdded](/hooks-tagadded)
matcher = { tag = "#urgent" }
command = "./hooks/notify.ts"Object matchers are permissive - only specified keys are enforced.
timeout (optional)
Maximum execution time in seconds. Default: no timeout.
[hooks.FileChanged](/hooks-filechanged)
command = "./hooks/slow-process.sh"
timeout = 60If the hook exceeds the timeout, it receives SIGKILL and the execution is logged as an error.
Matcher Reference
String Matchers (FileChanged)
Glob patterns matched against the file path:
| Pattern | Matches |
|---|---|
Mesh/*.md | Files directly in Mesh/ |
Mesh/**/*.md | All markdown files in Mesh/ recursively |
Mesh/Tasks/*.md | Task files |
*.md | Any markdown file |
Mesh/Plans/(Plan)*.md | Plan files with type prefix |
Object Matcher Fields
| Field | Event Types | Description |
|---|---|---|
tag | TagAdded, TagRemoved | Match specific tag value |
field | StatusChanged | Match the changed field name |
from | StatusChanged | Match previous value |
to | StatusChanged | Match new value |
Examples:
# Run when any task starts
[hooks.StatusChanged](/hooks-statuschanged)
matcher = { to = "in-progress" }
command = "./hooks/task-started.ts"
# Run when urgent tag is added
[hooks.TagAdded](/hooks-tagadded)
matcher = { tag = "#urgent" }
command = "./hooks/urgent-alert.ts"
# Run when task completes
[hooks.StatusChanged](/hooks-statuschanged)
matcher = { from = "in-progress", to = "done" }
command = "./hooks/task-done.ts"No Matcher
Omitting matcher runs the hook for all events of that type:
# Run for every file change
[hooks.FileChanged](/hooks-filechanged)
command = "./hooks/log-change.sh"Hook Input
Hooks receive JSON via stdin:
{
"event": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "FileChanged",
"timestamp": "2026-01-22T10:30:00.000Z",
"payload": {
"path": "Mesh/Tasks/(Task) 055 Implementation.md",
"tags": ["#task", "#proj/task", "#ld/living"],
"status": "in-progress"
}
},
"flint": {
"path": "/Users/me/my-flint"
}
}Event Payloads
FileChanged:
{
"path": "Mesh/Tasks/(Task) 055.md",
"tags": ["#task", "#ld/living"],
"status": "in-progress"
}StatusChanged:
{
"path": "Mesh/Tasks/(Task) 055.md",
"field": "status",
"from": "todo",
"to": "in-progress"
}TagAdded / TagRemoved:
{
"path": "Mesh/Tasks/(Task) 055.md",
"tag": "#urgent"
}AgentStart / AgentComplete:
{
"sessionId": "session-123",
"status": "active",
"model": "claude-3-opus",
"goal": "Implement feature X"
}ConnectionReceived:
{
"from": "Other Flint",
"subject": "Connection Event",
"path": "Connections/Other Flint/con-other-flint.md"
}RuntimeStart / RuntimeStop:
{
"flintPath": "/Users/me/my-flint"
}Hook Output
Exit Codes
| Code | Meaning | Logging |
|---|---|---|
0 | Success | Info |
2 | Block/soft-fail | Warning |
| Other | Error | Error |
Exit code 2 is useful for conditional logic - e.g., a validation hook that blocks an operation without being treated as an error.
stdout/stderr
Hook output is captured and logged by the runtime:
- stdout: Logged at info level
- stderr: Logged at error level
Execution Environment
Working Directory
Hooks run with cwd set to the Flint root.
PATH
The runtime extends PATH to include:
<flint-root>/node_modules/.bin- Workspace
node_modules/.bin
This allows hooks to use locally installed tools without full paths.
Shell
Commands run with shell: true, so shell features (pipes, redirects, etc.) work:
[hooks.FileChanged](/hooks-filechanged)
command = "echo 'Changed:' && cat /dev/stdin | jq '.event.payload.path'"Examples
Log All File Changes
[hooks.FileChanged](/hooks-filechanged)
command = "./hooks/log-change.sh"hooks/log-change.sh:
#!/bin/bash
read -r input
path=$(echo "$input" | jq -r '.event.payload.path')
echo "[$(date)] File changed: $path" >> .flint/change.logNotify on Urgent Tags
[hooks.TagAdded](/hooks-tagadded)
matcher = { tag = "#urgent" }
command = "./hooks/notify-urgent.ts"
timeout = 10hooks/notify-urgent.ts:
import { createInterface } from 'readline';
const rl = createInterface({ input: process.stdin });
rl.on('line', (line) => {
const data = JSON.parse(line);
const path = data.event.payload.path;
// Send notification (example: macOS)
const { execSync } = require('child_process');
execSync(`osascript -e 'display notification "Urgent: ${path}" with title "Flint"'`);
});Run Tests on Task Start
[hooks.StatusChanged](/hooks-statuschanged)
matcher = { from = "todo", to = "in-progress" }
command = "./hooks/run-tests.sh"
timeout = 300Sync on Agent Complete
[hooks.AgentComplete](/hooks-agentcomplete)
command = "./hooks/post-agent.ts"hooks/post-agent.ts:
import { createInterface } from 'readline';
import { execSync } from 'child_process';
const rl = createInterface({ input: process.stdin });
rl.on('line', (line) => {
const data = JSON.parse(line);
if (data.event.payload.status === 'complete') {
// Run sync after agent completes
execSync('flint sync', { stdio: 'inherit' });
}
});Hot Reload
The runtime watches flint-hooks.toml for changes. When the file is modified, hooks are reloaded automatically - no runtime restart needed.
Troubleshooting
Hook Not Firing
- Check event type matches your hook configuration
- Verify matcher pattern is correct
- Check file is in a watched path (Mesh/, sessions, connections)
- Look for errors in runtime logs
Permission Denied
Ensure hook scripts are executable:
chmod +x hooks/my-hook.shScript Not Found
- Verify path is relative to Flint root or absolute
- Check file extension is correct
- Ensure required runtime (tsx, python3, etc.) is installed
Timeout Issues
- Increase timeout value in configuration
- Move long-running work to background processes
- Consider async patterns (write to queue, process separately)
Related
- Module - Runtime - Runtime system overview
- Reference - CLI Commands - Server and runtime CLI commands