Skip to content

Customizing agent routing for your stack

FlowForge ships a routing hook that classifies every file write your Claude Code session attempts and dispatches it to the right specialist agent (per Rule #35). For most projects, the defaults work out-of-the-box. For projects with non-standard layouts, this page covers the override path.

If you only want to know “is my stack covered?”, jump to FF-shipped default patterns.

If you need to override, jump to Customer-config override.

When a Claude Code sub-agent attempts a Write, Edit, or MultiEdit against a file inside a registered FlowForge repo, the canonical hook at .flowforge/hooks/check-agent-requirement.sh fires as a PreToolUse hook. It classifies the file in priority order and emits a routing decision back to Claude Code:

  1. Project-scope guard — files outside any Git worktree, or outside a FlowForge-registered worktree, are skipped (no routing applied).
  2. Auto-generated short-circuitdist/, build/, .next/, node_modules/, *.g.dart etc. are never routed.
  3. Customer-config routing.paths — your project’s overrides (most-specific wins; see Conflict resolution).
  4. FF-shipped default-pattern library — Python, Node.js, Go path conventions.
  5. FF-internal path overrides — preserved for FF’s own dogfood (e.g., v3/internal/tui/*, v3/site/*).
  6. Customer-config routing.extensionMap — your project’s extension overrides.
  7. FF-shipped extension classifiers.py, .ts, .tsx, .go, .md, .mdx, etc.
  8. Advisory escalation — when nothing matches, the hook emits a non-blocking advisory and lets the write proceed.

The hook’s behavior is ratified in ADR-0031: Customer-Project Hook Routing Strategy. The implementation landed in PR #1209 (commit 3f4d9d06), built on the foundation from PR #1195 (commit d57d0463).

FlowForge ships built-in path conventions for three stacks at v1.0:

Path patternRoutes to
*/app/views/**fft-frontend (Django template views)
*/templates/**fft-frontend
*/migrations/**fft-database
*/tests/**/*.py, */tests/*.py, */test/**/*.py, */test/*.pyfft-qa
*.py (anywhere else)fft-backend
Path patternRoutes to
*/app/**fft-frontend (Next.js 13+ App Router)
*/pages/**fft-frontend (Next.js Pages Router and Express views)
*/__tests__/**fft-qa (when no coder agent is active)
*/migrations/**fft-database
*.tsx, *.jsx (anywhere else)fft-frontend
*.ts, *.js, *.mjs (anywhere else)fft-backend (or fft-frontend inside detected Vite projects)
Path patternRoutes to
*/cmd/**fft-backend
*/internal/**fft-backend
*/pkg/**fft-backend
*_test.gofft-qa (when no coder agent is active)
*.go (anywhere else)fft-backend

Why these three stacks? Per ADR-0031 §“Default-pattern library v1.0 scope rationale”, Python + Node.js + Go cover the v1.0 cascade-launch target audience. Rust, Rails, and other stacks are deferred to v1.1+ behind a customer-pain-threshold gate (≥3 customer reports or ≥1 paying-customer report). If your stack isn’t covered, the override path below takes 30 seconds.

Your project owns the final word on routing. Drop a .flowforge/config.json at your repo root with a routing namespace:

{
"routing": {
"paths": {
"src/backend/**": "fft-backend",
"src/frontend/**": "fft-frontend",
"internal/tui/**": "fft-tui",
"docs/**": "fft-documentation",
"tests/**": "fft-qa"
},
"extensionMap": {
".vue": "fft-frontend",
".rs": "fft-backend"
}
}
}

That’s it. The hook reloads the config on every fire (no daemon restart needed) and your rules apply immediately.

Maps glob patterns to specialist agent names. Glob semantics:

  • ** matches across path separators (e.g., src/**/*.py matches src/api/v1/users.py).
  • Patterns are matched against the repo-relative path of the file being written.
  • Absolute paths in glob keys are not supported — use repo-relative globs only. Absolute keys will silently not match files.

Maps file extensions (including the leading dot) to specialist agent names. Customer extension entries are merged on top of FF-shipped defaults — your keys win on collision; FF’s keys remain for extensions you didn’t override.

Your routing.paths wins over FF-shipped path overrides, including FF’s own dogfood patterns. This is the ratified priority-order in ADR-0031 §“Customer-config schema” — see the ‘Match priority order’ list and decision point #7 in §“Founder decision points” (ratified at issue #1193 comment-4418700341 on 2026-05-11).

The priority order (most-specific to least-specific) is:

customer routing.paths
> FF-shipped default-pattern library
> FF-internal path overrides (FF dogfood)
> customer routing.extensionMap
> FF-shipped extension classifiers
> advisory escalation

A customer with "v3/internal/tui/*": "fft-backend" in their fork wins over FF’s own */v3/internal/tui/*fft-tui override. This is intentional.

When two or more routing.paths entries match the same file, FlowForge uses longest-literal-prefix-wins: the prefix of each pattern up to the first wildcard segment is compared, and the longer literal prefix wins. Ties on literal-prefix length break by lexicographic order of the full pattern string (deterministic).

This matches the tsconfig.json paths semantic — most customers already think in directory-hierarchy specificity terms, and this rule formalizes that intuition.

Pattern APattern BFile matchedLiteral prefix ALiteral prefix BWinner
src/backend/**src/**src/backend/foo.pysrc/backend/src/A (longer literal prefix)
**/foo.pysrc/**src/foo.py“ (empty)src/B (literal prefix beats empty)
src/api/**src/**src/api/foo.pysrc/api/src/A
docs/architecture/**docs/**docs/architecture/foo.mddocs/architecture/docs/A

The literal prefix is computed by stripping everything from the first glob character (*, ?, [) onward. Pattern-author intent: more-specific path prefixes are the customer’s narrowing signal.

When the hook finishes its priority traversal and one of the conditions below holds, it emits a structured advisory event to ~/.flowforge/logs/routing-advisory.jsonl. The write is not blocked — advisory ≠ Rule #38 hard block, by explicit design.

ReasonTriggered when
customer_overrideCustomer-config routing.paths or routing.extensionMap matched, AND an FF-shipped default would have matched the same file with a different specialist. Tells you “your override is winning over FF’s default” — informational.
fallbackNo path-based rule matched; the hook fell through to extension-based classification or returned no agent. Tells you “consider adding a pattern for this path.”

ambiguous_match (multiple FF-shipped patterns matching with different agents) is reserved in the schema and will land in a follow-up — see FU ticket #1215.

{
"event": "routing_advisory",
"file": "<absolute-path>",
"selected_specialist": "<chosen-agent-or-empty>",
"alternatives": ["<other-agent-1>", "<other-agent-2>"],
"reason": "customer_override | fallback",
"timestamp": "<ISO8601-UTC>"
}

One event per line. Append-only. Non-blocking — the hook never fails on advisory-emit errors.

Terminal window
# Recent advisories
tail -20 ~/.flowforge/logs/routing-advisory.jsonl | jq .
# All fallbacks in the last day (paths with no rule)
grep '"reason":"fallback"' ~/.flowforge/logs/routing-advisory.jsonl | tail -50
# Files where your override won
grep '"reason":"customer_override"' ~/.flowforge/logs/routing-advisory.jsonl | jq '.file'

A future v1.1 TUI panel will surface advisories interactively; v1.0 ships with CLI-grep as the canonical surface.

Standard layout, not yet in FF’s default library:

{
"routing": {
"paths": {
"src/**": "fft-backend",
"tests/**": "fft-qa",
"benches/**": "fft-performance"
},
"extensionMap": {
".rs": "fft-backend"
}
}
}

Convention-over-configuration meets explicit-over-default:

{
"routing": {
"paths": {
"app/controllers/**": "fft-backend",
"app/views/**": "fft-frontend",
"app/models/**": "fft-database",
"db/migrate/**": "fft-database",
"spec/**": "fft-qa"
}
}
}

Override the default Node.js routing to add Vue’s component tree:

{
"routing": {
"paths": {
"components/**": "fft-frontend",
"composables/**": "fft-frontend",
"server/api/**": "fft-backend"
},
"extensionMap": {
".vue": "fft-frontend"
}
}
}

routing.paths matches against repo-relative paths, so monorepo subdirectories work naturally:

{
"routing": {
"paths": {
"apps/web/**": "fft-frontend",
"apps/api/**": "fft-backend",
"packages/db/**": "fft-database",
"packages/shared/**": "fft-architecture"
}
}
}

The simplest possible .flowforge/config.json adds ONE rule to FF’s defaults — useful for a one-off path the FF-shipped library doesn’t cover:

{
"routing": {
"paths": {
"scripts/migrations/**": "fft-database"
}
}
}

This adds a single mapping (scripts/migrations/**fft-database) and inherits all FF-shipped defaults for everything else (Python src/**/*.pyfft-backend, Node __tests__/**fft-qa, etc.). Reach for this pattern when you have ONE narrow exception to FF’s defaults; reach for the larger override examples (Rust/Rails/Vue) only when your stack falls outside FF’s v1.0 default library.

Your .flowforge/config.json can hold multiple namespaces side-by-side. The routing namespace evolves independently from milestone (per ADR-0030 §6.10 Amendment 1) — a v2 of routing schema does not force a v2 of milestone schema, and vice versa. That’s the explicit benefit of named-key versioning:

{
"routing": {
"paths": {
"src/backend/**": "fft-backend"
}
},
"milestone": {
"attributionGlobs": {
"adr": ["documentation/architecture/ADR-*.md"],
"prd": ["documentation/architecture/PRD-*.md"]
}
}
}

v1.0 does NOT introduce a schemaVersion field — the presence of the routing object at top level implies schema v1. This follows the ADR-0030 §6.10 Amendment 1 precedent verbatim.

When a future v2 becomes necessary, the upgrade path is:

  1. Add "schemaVersion": 2 inside the routing object (not at the top level — keeps routing config namespaced).
  2. Implementations encountering schemaVersion: 1 (or absent — equivalent) MUST continue to parse the v1 shape unchanged.
  3. Implementations encountering schemaVersion: 2 MUST follow the v2-ratifying ADR’s shape definition.

This is additive: today’s routing block keeps working forever.

If the routing hook blocks your sub-agent’s write with a “required agent” message, this is a Rule #38 security boundary. The canonical pattern (per FlowForge’s internal Rule #38 hook governance contract — see ADR-0031 §“Cross-references” for full background) is:

  1. Do not disable the hook, write to ~/.flowforge/.agent-auth/, set FLOWFORGE_BYPASS=*, or otherwise work around the block.
  2. Do escalate: the controlling agent (the maestro) re-dispatches the work to the correct specialist agent named in the block message.

If you genuinely need a different routing rule, add it to your .flowforge/config.json — that’s the legitimate path. If you believe the hook is wrong, file a DX ticket — let the controller re-dispatch you with corrected scope. “The hook is wrong” is not a license to bypass, even when you believe the hook is buggy.

Check the priority order. routing.paths wins over routing.extensionMap, so if you have BOTH a path glob and an extension map entry that match the same file, the path wins. To force extension-only routing for a specific extension, leave routing.paths empty for that file’s path tree.

Three common causes:

  1. Absolute path in the glob key — use repo-relative paths only.
  2. Glob assumes ** works without separators** matches across / characters; * does not.
  3. Pattern matches a dist/ or node_modules/ short-circuit — auto-generated paths are filtered before customer routing fires. This is intentional.

Run a one-shot debug:

Terminal window
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$(pwd)/<your-file>"'"}}' \
| bash .flowforge/hooks/check-agent-requirement.sh

The hook prints its decision to stdout and logs the priority-traversal trace to ~/.flowforge/logs/claude-pretool.log.

”I’m seeing advisories I don’t understand”

Section titled “”I’m seeing advisories I don’t understand””

Each advisory carries a reason field telling you why it fired. customer_override is informational — your config is winning, as designed. fallback is a hint — the hook couldn’t classify the path; consider adding it to your config.

If advisory volume is too noisy, you can disable the JSONL emit by truncating the log file; the hook never crashes on log-write failure. Future v1.1 ships a TUI surface that aggregates and de-duplicates advisories per cascade-window retrospective.