FlintNUU Flint Docs
Reference

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>               # optional

Event Types

Event TypeDescription
FileChangedMarkdown file in watched paths changed
StatusChangedFrontmatter status field changed
TagAddedTag added to frontmatter
TagRemovedTag removed from frontmatter
AgentStartAgent session started
AgentCompleteAgent session completed
ConnectionReceivedConnection event received
RuntimeStartRuntime started
RuntimeStopRuntime 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:

ExtensionExecution
.tstsx <file>
.pypython3 <file>
.shbash <file>
OtherDirect 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 = 60

If 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:

PatternMatches
Mesh/*.mdFiles directly in Mesh/
Mesh/**/*.mdAll markdown files in Mesh/ recursively
Mesh/Tasks/*.mdTask files
*.mdAny markdown file
Mesh/Plans/(Plan)*.mdPlan files with type prefix

Object Matcher Fields

FieldEvent TypesDescription
tagTagAdded, TagRemovedMatch specific tag value
fieldStatusChangedMatch the changed field name
fromStatusChangedMatch previous value
toStatusChangedMatch 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

CodeMeaningLogging
0SuccessInfo
2Block/soft-failWarning
OtherErrorError

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.log

Notify on Urgent Tags

[hooks.TagAdded](/hooks-tagadded)
matcher = { tag = "#urgent" }
command = "./hooks/notify-urgent.ts"
timeout = 10

hooks/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 = 300

Sync 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

  1. Check event type matches your hook configuration
  2. Verify matcher pattern is correct
  3. Check file is in a watched path (Mesh/, sessions, connections)
  4. Look for errors in runtime logs

Permission Denied

Ensure hook scripts are executable:

chmod +x hooks/my-hook.sh

Script 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)