Hooks
Automated quality gates and workflow automation at every stage of development.
fspec provides two types of hooks that let you automate checks, run tests, format code, and enforce standards throughout your development workflow. Understanding when to use each type is key to building an effective quality system.
The Two Types of Hooks
Global Lifecycle Hooks
Global hooks are permanent, project-wide automation that runs for all work units. They enforce standards, run CI/CD checks, and maintain consistent quality across your entire codebase.
Think of them as your project's foundational quality layer—the checks that every piece of work must pass, regardless of which feature or bug you're working on.
Configured in: spec/fspec-hooks.json
Characteristics:
- Apply to all work units in the project
- Permanent until manually removed
- Can have conditions (tags, prefixes, epics, estimate ranges)
- Require manual script creation
- Perfect for team standards and CI/CD integration
Virtual Hooks
Virtual hooks are temporary, work unit-specific checks that AI can attach dynamically. They're ephemeral quality gates for individual stories, features, or bug fixes.
Think of them as temporary scaffolding—you add them when you need specific checks for a particular piece of work, then remove them when the work is done.
Configured in: spec/work-units.json (per work unit)
Characteristics:
- Apply to one work unit only
- Temporary (removed when work reaches "done")
- Can auto-generate scripts for git context
- Managed entirely through CLI commands
- Perfect for story-specific checks and experiment with new quality gates
When to Use Each Type
Use this natural language decision process:
YOU: "I want to run eslint before every validation."
Think: Does this apply to ALL work units or just this one?
Answer: All work units → Use global hook
---
YOU: "For this authentication story, run security audit before validating."
Think: Does this apply to ALL work units or just this one?
Answer: Just this one story → Use virtual hook
---
YOU: "I want to run prettier on all TypeScript files in the project."
Think: Is this a permanent standard or temporary check?
Answer: Permanent standard → Use global hook
---
YOU: "For this performance work unit, check bundle size before validating."
Think: Is this a permanent standard or temporary check?
Answer: Temporary for this work unit → Use virtual hookHow the Hook Lifecycle Works
Every time you run a command that can trigger hooks, fspec goes through a two-phase process: discovery and execution.
Phase 1: Discovery
First, fspec discovers which hooks should run:
What this means in practice:
When you run a command like fspec move AUTH-001 implementing, fspec:
- Generates event names:
pre-implementingandpost-implementing - Loads global hooks: Reads
spec/fspec-hooks.jsonto find project-wide hooks - Loads the work unit: Reads AUTH-001's data from
spec/work-units.json - Discovers virtual hooks: Finds any virtual hooks attached to AUTH-001 for these events
- Discovers global hooks: Finds any global hooks configured for these events
- Filters by conditions: If global hooks have conditions (like "only for @security tags"), checks if they match
- Prepares execution list: Now knows exactly which hooks to run
Phase 2: Execution
Then, fspec executes hooks in a specific order:
Execution order is critical:
- Virtual hooks run FIRST (work unit-specific checks)
- Global hooks run SECOND (project-wide checks)
- Within each category, hooks run in array order (order they were added)
Why this order?
- Virtual hooks are more specific to the current work, so they should fail fast
- If a virtual hook fails, there's no point running global hooks
- Global hooks enforce project-wide standards after work unit-specific checks pass
Hook Events
Hooks trigger at specific moments in your workflow. The event names follow a simple pattern:
pre-<command-name> (runs BEFORE the command executes)
post-<command-name> (runs AFTER the command succeeds)Common Events
Here are the most useful events for natural language workflows:
Status Change Events:
pre-implementing Before work unit moves to implementing
post-implementing After work unit moves to implementing
pre-validating Before work unit moves to validating
post-validating After work unit moves to validating
pre-testing Before work unit moves to testing
post-testing After work unit moves to testing
pre-specifying Before work unit moves to specifying
post-specifying After work unit moves to specifyingWhat this looks like in conversation:
YOU: "When I finish implementing AUTH-001, run the test suite."
AI: I'll add a virtual hook to run tests after implementing...
fspec add-virtual-hook AUTH-001 post-implementing "npm test" --blockingGlobal Hooks in Detail
Setting Up Global Hooks
Global hooks are configured in spec/fspec-hooks.json:
{
"global": {
"timeout": 120,
"shell": "/bin/bash"
},
"hooks": {
"post-implementing": [
{
"name": "run-tests",
"command": "spec/hooks/run-tests.sh",
"blocking": true,
"timeout": 60
}
],
"pre-validating": [
{
"name": "lint-code",
"command": "eslint src/",
"blocking": true,
"condition": {
"tags": ["@frontend"]
}
}
]
}
}Breaking this down:
- global.timeout: Default timeout for all hooks (120 seconds)
- global.shell: Shell to use for executing hooks
- hooks: Object mapping event names to arrays of hook definitions
Each hook has:
- name: Identifier for the hook
- command: Command to run (shell command or path to script)
- blocking: If true, failure prevents workflow progression
- timeout: Override global timeout for this hook
- condition: Optional conditions for when the hook should run
Conditional Execution
Global hooks can be conditionally executed based on work unit properties:
{
"name": "security-scan",
"command": "npm audit",
"blocking": true,
"condition": {
"tags": ["@security", "@auth"],
"prefix": ["AUTH", "SEC"],
"epic": "security-hardening",
"estimateMin": 3,
"estimateMax": 8
}
}Condition logic:
- tags: Hook runs if work unit has ANY of these tags (OR logic)
- prefix: Hook runs if work unit ID starts with ANY prefix (OR logic)
- epic: Hook runs if work unit belongs to this epic
- estimateMin/Max: Hook runs if estimate is within this range
- Multiple conditions use AND logic (all must match)
In conversation:
YOU: "Only run the security audit for work units tagged @security."
AI: I'll add a condition to the security-scan global hook:
{
"name": "security-scan",
"command": "npm audit",
"blocking": true,
"condition": {
"tags": ["@security"]
}
}
This hook will only run for work units with the @security tag.Virtual Hooks in Detail
Creating Virtual Hooks
Virtual hooks are created using the CLI:
fspec add-virtual-hook <work-unit-id> <event> <command> [--blocking] [--git-context]Examples:
# Run tests after implementing
fspec add-virtual-hook AUTH-001 post-implementing "npm test" --blocking
# Lint changed files before validating
fspec add-virtual-hook AUTH-001 pre-validating "eslint" --git-context --blocking
# Send notification after validating (non-blocking)
fspec add-virtual-hook AUTH-001 post-validating "npm run notify"Git Context: Smart File-Aware Hooks
The --git-context flag is powerful. It auto-generates a script that:
- Receives git status from fspec (staged and unstaged files)
- Filters only the files you've changed
- Passes those files to your command
Example:
fspec add-virtual-hook AUTH-001 pre-validating "eslint" --git-context --blockingThis generates spec/hooks/.virtual/AUTH-001-eslint.sh:
#!/bin/bash
set -e
# Read context JSON from stdin
CONTEXT=$(cat)
# Extract staged and unstaged files
STAGED_FILES=$(echo "$CONTEXT" | jq -r '.stagedFiles[]? // empty')
UNSTAGED_FILES=$(echo "$CONTEXT" | jq -r '.unstagedFiles[]? // empty')
# Combine all changed files
ALL_FILES="$STAGED_FILES $UNSTAGED_FILES"
# Exit if no files to process
if [ -z "$ALL_FILES" ]; then
echo "No changed files to process"
exit 0
fi
# Run command with changed files
eslint $ALL_FILESWhy use git context?
- Efficiency: Only check changed files, not the entire codebase
- Relevance: Focus on work-in-progress
- Speed: Faster feedback for AI agents
Managing Virtual Hooks
List hooks for a work unit:
fspec list-virtual-hooks AUTH-001Output:
Virtual hooks for AUTH-001:
🪝 eslint (pre-validating, blocking, git context)
Command: eslint
Script: spec/hooks/.virtual/AUTH-001-eslint.sh
🪝 npm test (post-implementing, blocking)
Command: npm testRemove a specific hook:
fspec remove-virtual-hook AUTH-001 eslintClear all hooks from a work unit:
fspec clear-virtual-hooks AUTH-001Copy hooks between work units:
# Copy all hooks
fspec copy-virtual-hooks --from AUTH-001 --to AUTH-002
# Copy specific hook only
fspec copy-virtual-hooks --from AUTH-001 --to AUTH-002 --hook-name eslintBlocking vs Non-Blocking Hooks
Hooks can either block workflow progression or just warn you about failures.
Blocking Hooks (--blocking flag)
When to use:
- Critical quality checks (linting, tests, type checking)
- Security scans that must pass
- Formatting requirements
Behavior:
- Pre-hooks: Failure prevents the command from executing entirely
- Post-hooks: Failure sets exit code to 1
- AI sees failures wrapped in
<system-reminder>tags for high visibility
Example:
fspec add-virtual-hook AUTH-001 post-implementing "npm test" --blockingIf tests fail:
<system-reminder>
BLOCKING HOOK FAILURE: Virtual hook 'npm test' for AUTH-001 failed.
Stderr:
AuthService.test.ts:
✕ should validate user credentials (42ms)
Expected: true
Received: false
This is a BLOCKING hook. Fix the errors before proceeding.
</system-reminder>Non-Blocking Hooks (default)
When to use:
- Notifications (Slack, email)
- Metrics collection
- Optional checks
- Warnings that shouldn't stop work
Behavior:
- Failure is logged but doesn't prevent progression
- Useful for "nice to have" checks
Example:
fspec add-virtual-hook AUTH-001 post-validating "npm run notify"Natural Language Workflows
Here's how you'd use hooks in conversational development:
Scenario 1: Adding Quality Gates During Specifying
YOU: "Let's spec out the user authentication story."
AI: I'll create a story for user authentication...
[AI creates story AUTH-001]
AI: Would you like me to run any commands at specific stages of this story?
For example, I could run tests after implementing, or lint before validating.
YOU: "Yes, run eslint on changed files before validating, and run tests after implementing."
AI: I'll add those virtual hooks:
fspec add-virtual-hook AUTH-001 pre-validating "eslint" --git-context --blocking
fspec add-virtual-hook AUTH-001 post-implementing "npm test" --blocking
✓ Added pre-validating hook: eslint (git context, blocking)
✓ Added post-implementing hook: npm test (blocking)
These hooks will automatically run during the workflow.Scenario 2: Handling Hook Failures
YOU: "Move AUTH-001 to implementing."
AI: Moving AUTH-001 to implementing...
fspec move AUTH-001 implementing
[Hook runs: npm test]
<system-reminder>
BLOCKING HOOK FAILURE: Virtual hook 'npm test' for AUTH-001 failed.
Stderr:
✕ should validate email format
Expected: true
Received: false
</system-reminder>
AI: The tests failed. I need to fix the email validation logic before we can proceed.
Looking at the test failure, it seems the email validation regex is incorrect.
Let me fix that...
[AI fixes the code]
Now let me try moving to implementing again:
fspec move AUTH-001 implementing
✓ Tests passed! AUTH-001 is now in implementing state.Scenario 3: Cleanup After Completion
YOU: "AUTH-001 is done, move it to done status."
AI: Moving AUTH-001 to done...
fspec move AUTH-001 done
✓ AUTH-001 moved to done!
I notice AUTH-001 has 2 virtual hooks:
- eslint (pre-validating, blocking)
- npm test (post-implementing, blocking)
Should I remove these hooks now that the work is complete?
YOU: "Yes, clean them up."
AI: Removing virtual hooks...
fspec clear-virtual-hooks AUTH-001
✓ Cleared 2 virtual hook(s) from AUTH-001
✓ Deleted 1 generated script(s)
AUTH-001 is now clean and complete!Common Patterns
Pattern 1: Full Quality Pipeline
Stack multiple checks for comprehensive quality:
fspec add-virtual-hook AUTH-001 post-implementing "npm run lint" --blocking
fspec add-virtual-hook AUTH-001 post-implementing "npm run typecheck" --blocking
fspec add-virtual-hook AUTH-001 post-implementing "npm test" --blocking
fspec add-virtual-hook AUTH-001 pre-validating "npm run format-check" --blockingExecution flow:
Implementing → Lint → Type Check → Tests → Validating → Format Check → DonePattern 2: Security-Specific Checks
Only run security scans for auth-related work:
Global hook with condition:
{
"name": "security-audit",
"command": "npm audit",
"blocking": true,
"condition": {
"prefix": ["AUTH", "SEC"]
}
}Pattern 3: Git Context for Efficiency
Lint only changed files:
fspec add-virtual-hook UI-042 pre-validating "eslint" --git-context --blocking
fspec add-virtual-hook UI-042 pre-validating "prettier" --git-context --blockingPattern 4: Template Hooks for Related Work
Create a template, then copy:
# Set up template
fspec add-virtual-hook TEMPLATE-001 post-implementing "npm run lint" --blocking
fspec add-virtual-hook TEMPLATE-001 pre-validating "npm run typecheck" --blocking
# Copy to actual work units
fspec copy-virtual-hooks --from TEMPLATE-001 --to AUTH-001
fspec copy-virtual-hooks --from TEMPLATE-001 --to AUTH-002
fspec copy-virtual-hooks --from TEMPLATE-001 --to AUTH-003Decision Flow: Choosing the Right Hook
Best Practices
✅ DO
- Add virtual hooks during specifying - Set up quality gates before you start implementing
- Use
--blockingfor critical checks - Tests, linting, type checking should block progression - Use
--git-contextfor file commands - More efficient than checking the entire codebase - Remove virtual hooks when work is done - Keep your work units clean
- Start with virtual hooks - Experiment first, then promote successful checks to global hooks
- Use descriptive hook names - Makes debugging easier
❌ DON'T
- Skip cleanup - Remove virtual hooks when work reaches "done"
- Use virtual hooks for project standards - Use global hooks for permanent checks
- Manually edit generated scripts - They're regenerated automatically
- Forget to test hooks - Run them manually first to verify they work
- Over-block - Not every check needs to be blocking
Troubleshooting
Hook Not Executing
Check if it exists:
fspec list-virtual-hooks AUTH-001Verify command is accessible:
which eslint # Should print a pathCheck the event name matches:
Hook event: pre-validating
Command: fspec move AUTH-001 validating ✓ Matches!Git Context Failing
Verify jq is installed:
which jq # Required for JSON parsingCheck files actually changed:
git status # Should show modified filesInspect generated script:
cat spec/hooks/.virtual/AUTH-001-eslint.shDebugging Hooks
Test a hook manually:
# For git context hooks
echo '{"stagedFiles":["src/auth.ts"],"unstagedFiles":[]}' | \
spec/hooks/.virtual/AUTH-001-eslint.shCheck hook output:
Virtual hooks wrap stderr in <system-reminder> tags, making failures highly visible to AI.
Summary
fspec's hook system gives you two complementary tools:
Global Hooks: Permanent, project-wide automation for team standards and CI/CD integration.
Virtual Hooks: Temporary, work unit-specific checks that AI can attach dynamically.
Together, they create a comprehensive quality system that:
- Enforces standards automatically
- Provides fast feedback
- Prevents bugs from reaching production
- Adapts to different types of work
- Integrates seamlessly with natural language workflows
The key insight: Start with virtual hooks for experimentation, promote successful patterns to global hooks for permanent standards.