Features
Hooks

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 hook

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

  1. Generates event names: pre-implementing and post-implementing
  2. Loads global hooks: Reads spec/fspec-hooks.json to find project-wide hooks
  3. Loads the work unit: Reads AUTH-001's data from spec/work-units.json
  4. Discovers virtual hooks: Finds any virtual hooks attached to AUTH-001 for these events
  5. Discovers global hooks: Finds any global hooks configured for these events
  6. Filters by conditions: If global hooks have conditions (like "only for @security tags"), checks if they match
  7. 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:

  1. Virtual hooks run FIRST (work unit-specific checks)
  2. Global hooks run SECOND (project-wide checks)
  3. 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 specifying

What 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" --blocking

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

  1. Receives git status from fspec (staged and unstaged files)
  2. Filters only the files you've changed
  3. Passes those files to your command

Example:

fspec add-virtual-hook AUTH-001 pre-validating "eslint" --git-context --blocking

This 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_FILES

Why 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-001

Output:

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 test

Remove a specific hook:

fspec remove-virtual-hook AUTH-001 eslint

Clear all hooks from a work unit:

fspec clear-virtual-hooks AUTH-001

Copy 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 eslint

Blocking 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" --blocking

If 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" --blocking

Execution flow:

Implementing → Lint → Type Check → Tests → Validating → Format Check → Done

Pattern 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 --blocking

Pattern 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-003

Decision Flow: Choosing the Right Hook

Best Practices

✅ DO

  • Add virtual hooks during specifying - Set up quality gates before you start implementing
  • Use --blocking for critical checks - Tests, linting, type checking should block progression
  • Use --git-context for 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-001

Verify command is accessible:

which eslint  # Should print a path

Check 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 parsing

Check files actually changed:

git status  # Should show modified files

Inspect generated script:

cat spec/hooks/.virtual/AUTH-001-eslint.sh

Debugging Hooks

Test a hook manually:

# For git context hooks
echo '{"stagedFiles":["src/auth.ts"],"unstagedFiles":[]}' | \
  spec/hooks/.virtual/AUTH-001-eslint.sh

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