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.
What the routing hook does
Section titled “What the routing hook does”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:
- Project-scope guard — files outside any Git worktree, or outside a FlowForge-registered worktree, are skipped (no routing applied).
- Auto-generated short-circuit —
dist/,build/,.next/,node_modules/,*.g.dartetc. are never routed. - Customer-config
routing.paths— your project’s overrides (most-specific wins; see Conflict resolution). - FF-shipped default-pattern library — Python, Node.js, Go path conventions.
- FF-internal path overrides — preserved for FF’s own dogfood (e.g.,
v3/internal/tui/*,v3/site/*). - Customer-config
routing.extensionMap— your project’s extension overrides. - FF-shipped extension classifiers —
.py,.ts,.tsx,.go,.md,.mdx, etc. - 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).
FF-shipped default patterns
Section titled “FF-shipped default patterns”FlowForge ships built-in path conventions for three stacks at v1.0:
Python (Django / FastAPI / Flask)
Section titled “Python (Django / FastAPI / Flask)”| Path pattern | Routes to |
|---|---|
*/app/views/** | fft-frontend (Django template views) |
*/templates/** | fft-frontend |
*/migrations/** | fft-database |
*/tests/**/*.py, */tests/*.py, */test/**/*.py, */test/*.py | fft-qa |
*.py (anywhere else) | fft-backend |
Node.js (Next.js / Express)
Section titled “Node.js (Next.js / Express)”| Path pattern | Routes 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) |
Go modules
Section titled “Go modules”| Path pattern | Routes to |
|---|---|
*/cmd/** | fft-backend |
*/internal/** | fft-backend |
*/pkg/** | fft-backend |
*_test.go | fft-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.
Customer-config override
Section titled “Customer-config override”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.
routing.paths
Section titled “routing.paths”Maps glob patterns to specialist agent names. Glob semantics:
**matches across path separators (e.g.,src/**/*.pymatchessrc/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.
routing.extensionMap
Section titled “routing.extensionMap”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.
Customer-priority
Section titled “Customer-priority”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 escalationA customer with "v3/internal/tui/*": "fft-backend" in their fork wins over FF’s own */v3/internal/tui/* → fft-tui override. This is intentional.
Conflict resolution
Section titled “Conflict resolution”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 A | Pattern B | File matched | Literal prefix A | Literal prefix B | Winner |
|---|---|---|---|---|---|
src/backend/** | src/** | src/backend/foo.py | src/backend/ | src/ | A (longer literal prefix) |
**/foo.py | src/** | src/foo.py | “ (empty) | src/ | B (literal prefix beats empty) |
src/api/** | src/** | src/api/foo.py | src/api/ | src/ | A |
docs/architecture/** | docs/** | docs/architecture/foo.md | docs/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.
Advisory escalation
Section titled “Advisory escalation”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.
When advisories fire
Section titled “When advisories fire”| Reason | Triggered when |
|---|---|
customer_override | Customer-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. |
fallback | No 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.
JSONL schema
Section titled “JSONL schema”{ "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.
Reading the log
Section titled “Reading the log”# Recent advisoriestail -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 wongrep '"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.
Customer worked examples
Section titled “Customer worked examples”Rust (Cargo) project
Section titled “Rust (Cargo) project”Standard layout, not yet in FF’s default library:
{ "routing": { "paths": { "src/**": "fft-backend", "tests/**": "fft-qa", "benches/**": "fft-performance" }, "extensionMap": { ".rs": "fft-backend" } }}Rails project
Section titled “Rails project”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" } }}Vue / Nuxt project
Section titled “Vue / Nuxt project”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" } }}Monorepo with mixed stacks
Section titled “Monorepo with mixed stacks”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" } }}Minimal override — single rule
Section titled “Minimal override — single rule”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/**/*.py → fft-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.
Coexistence with other FlowForge config
Section titled “Coexistence with other FlowForge config”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"] } }}Schema versioning
Section titled “Schema versioning”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:
- Add
"schemaVersion": 2inside theroutingobject (not at the top level — keeps routing config namespaced). - Implementations encountering
schemaVersion: 1(or absent — equivalent) MUST continue to parse the v1 shape unchanged. - Implementations encountering
schemaVersion: 2MUST follow the v2-ratifying ADR’s shape definition.
This is additive: today’s routing block keeps working forever.
Rule #38 — escalate, never work around
Section titled “Rule #38 — escalate, never work around”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:
- Do not disable the hook, write to
~/.flowforge/.agent-auth/, setFLOWFORGE_BYPASS=*, or otherwise work around the block. - 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.
Troubleshooting
Section titled “Troubleshooting””My override isn’t winning”
Section titled “”My override isn’t winning””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.
”My glob isn’t matching”
Section titled “”My glob isn’t matching””Three common causes:
- Absolute path in the glob key — use repo-relative paths only.
- Glob assumes
**works without separators —**matches across/characters;*does not. - Pattern matches a
dist/ornode_modules/short-circuit — auto-generated paths are filtered before customer routing fires. This is intentional.
Run a one-shot debug:
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$(pwd)/<your-file>"'"}}' \ | bash .flowforge/hooks/check-agent-requirement.shThe 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.
References
Section titled “References”- ADR-0031: Customer-Project Hook Routing Strategy — full architectural decision, three-option analysis, default-library scope rationale, and founder decision points.
- ADR-0030 §6.10 Amendment 1 — the versioned-namespaced-key precedent this schema consumes.
- ADR-0026:
~/.claude/Mirror Layer — how FF ships the routing hook to your machine. - PR #1209: Phase 3 implementation — the canonical hook code that implements this contract.
- PR #1195: Phase 1+2 foundation — the FF-internal
v3/siteoverride that established the path-override pattern. - Multi-project guide — how FF integrates across multiple registered repos.