claude-dispatch
v0.1.2
Published
Context-aware skill router for Claude Code hooks
Maintainers
Readme
claude-dispatch
Context-aware skill router for Claude Code hooks.
What it does
claude-dispatch is a routing engine that automatically matches user prompts to relevant skills (custom commands, agents, workflows) in Claude Code. It installs as a Claude Code hook and intercepts every prompt to check whether a specialized skill should handle it, presenting matches to the user before activation.
The router reads a single config file (dispatch-rules.json) that defines your skills, their trigger keywords, regex patterns, and contextual signals like directory paths, file types, and project markers. When a prompt comes in, the router scores it against all rules and returns the top matches with enforcement levels (suggest, silent, or block).
All routing happens in a Node.js hook process outside of Claude's context window. This means zero token cost -- the router never consumes Claude tokens for matching. It only adds context to the conversation when a match is found, and even then it is a small structured payload rather than a full skill definition.
Installation
npm install -g claude-dispatchThis makes the claude-dispatch command available system-wide. Requires Node.js >= 18.
Alternative: install from GitHub or clone for development
# Install directly from GitHub (latest, including unreleased changes)
npm install -g github:wwadley-lucas/claude-dispatch
# Or clone for development
git clone https://github.com/wwadley-lucas/claude-dispatch.git
cd claude-dispatch
npm install
npm link # makes 'claude-dispatch' available globallyVerify installation
claude-dispatch --versionArchitecture
User Prompt
|
v
+----------------------------+
| Layer 1: Keyword + Regex | keywords: +1 each
| | regex patterns: +2 each
+----------------------------+
|
v
+----------------------------+
| Layer 1.5: Context Signals | directory path boosts
| | file type detection
| | project markers (file presence/absence)
| | skill sequence history
+----------------------------+
|
v (only if L1+L1.5 found nothing and llmFallback: true)
+----------------------------+
| Layer 2: LLM Fallback | claude --print -m haiku
| | (optional, disabled by default)
+----------------------------+
|
v
Matched skills returned to Claude Code
with enforcement level and instructionsLayer 1 runs keyword word-boundary matching (+1 per hit) and regex pattern matching (+2 per hit) against the raw prompt text. Keywords match whole words only (e.g., "test" matches "write tests" but not "contest"). This is fast string/regex work in Node.js.
Layer 1.5 takes the Layer 1 results and applies contextual boosts or penalties based on the current working directory, file types present in the directory, project marker files (like package.json or .git), and skill sequence history (what skill ran last in this session). These signals adjust scores up or down by category.
Layer 2 is an optional LLM fallback (disabled by default). If Layers 1 and 1.5 found no matches and llmFallback is enabled, the router calls claude --print -m haiku with the prompt and rule list for a lightweight classification pass.
All three layers run in the hook process. Layers 1 and 1.5 consume zero Claude tokens. Layer 2 uses a small Haiku call only when enabled and only when the first two layers found nothing.
Quick Start
claude-dispatch initThis creates two files in your project:
.claude/hooks/context-router.js # The hook (self-contained, no node_modules needed)
.claude/dispatch-rules.json # Your routing config (12 starter rules included)Verify it works:
claude-dispatch test "deploy to production"Expected output:
Prompt: "deploy to production"
CWD: /your/project
Matches:
------------------------------------------------------------
1. Deployment (deployment)
command: deploy
score: 4 (layer1: 4, context: 0)
layer: 1
matched: deploy, production, /\bdeploy\s+to\s+(production|staging|prod)\b/The init command automatically wires the hook into .claude/settings.json, creating the file if needed:
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"command": "node .claude/hooks/context-router.js"
}
]
}
}If .claude/settings.json already exists with other settings, init merges the hook entry without overwriting your existing configuration.
CLI Commands
init
Scaffold the hook and config into your project.
claude-dispatch initCreates .claude/hooks/context-router.js and .claude/dispatch-rules.json (with 12 starter rules). Skips config creation if the file already exists.
Flags:
| Flag | Description |
|------|-------------|
| --update | Replace the hook file only. Preserves your dispatch-rules.json. Use this to upgrade the router after updating (npm update -g claude-dispatch). |
| --force | Overwrite everything including config. No confirmation prompt. |
# Upgrade hook without touching config
claude-dispatch init --update
# Full reset (will prompt before overwriting config)
claude-dispatch init --forcevalidate
Check your dispatch-rules.json for errors.
claude-dispatch validateChecks performed:
- Valid JSON syntax
- Schema version is 2
- All required rule fields present (
id,name,category,command,enforcement,keywords,patterns,description) - No duplicate rule IDs
- All regex patterns compile without errors
- Valid enforcement values (
suggest,silent,block) - Optional sections (
directorySignals,fileTypeSignals,skillSequences,projectMarkers) follow their schemas
Exits with code 0 on success, 1 on failure. Suitable for CI pipelines.
Flags:
| Flag | Description |
|------|-------------|
| -f <path> | Validate a config file at a custom path instead of .claude/dispatch-rules.json. |
# Validate default location
claude-dispatch validate
# Validate a specific file
claude-dispatch validate -f ./my-rules.jsontest
Dry-run a prompt through the router and see what matches.
claude-dispatch test "write tests before implementing the feature"Shows keyword hits, regex matches, context signal boosts, and the final ranked list. Uses your current working directory for context signal evaluation (directory signals, file types, project markers).
Flags:
| Flag | Description |
|------|-------------|
| -f <path> | Use a config file at a custom path. |
claude-dispatch test "refactor this module to reduce complexity"
claude-dispatch test -f ./custom-rules.json "check for security vulnerabilities"Example output:
Prompt: "write tests before implementing the feature"
CWD: /home/user/my-project
Matches:
------------------------------------------------------------
1. Test-Driven Development (tdd-workflow)
command: superpowers:test-driven-development
score: 4 (layer1: 4, context: 0)
layer: 1
matched: test, tdd, /\b(write|add|create)\s+tests?\s+(first|before)\b/add-rule
Interactively create a new routing rule.
claude-dispatch add-ruleWalks you through each field:
? Rule name: My Custom Workflow
Auto-generated ID: my-custom-workflow
? Category: dev-workflows
? Command to invoke: my-workflow
? Keywords (comma-separated): build, compile, make, webpack
? Regex patterns (comma-separated, optional): \bbuild\s+(this|the)\b
? Enforcement level: suggest
? Minimum score threshold: 2
Rule added to .claude/dispatch-rules.json
Running validation... OKThe rule is appended to your config and validated automatically.
create
Create a new skill or agent file and its routing rule in one step.
claude-dispatch createThe wizard walks you through the full flow:
? What are you creating? skill
? Name: Deploy Helper
? Description: Guides deployment to staging and production
? Category: dev-workflows
? Skill command: deploy-helper
? Keywords: deploy, staging, production, release
? Regex patterns (optional): \bdeploy\s+to\b
? Enforcement: suggest
? Min score: 2
Write to .claude/commands/deploy-helper.md? (Y/n) Y
Created: .claude/commands/deploy-helper.md
Rule added: "deploy-helper" → .claude/dispatch-rules.json
Validation: passed
Auto-test: "deploy staging production release"
✓ deploy-helper matchedCreates the markdown file (.claude/commands/ for skills, .claude/agents/ for agents), adds the routing rule, validates the config, and auto-tests that the new rule matches.
Configuration Reference
All routing configuration lives in a single file: .claude/dispatch-rules.json.
Full Schema
{
"version": 2,
"config": {
"maxMatches": 5,
"minScore": 2,
"cacheTTL": 300000,
"llmFallback": false,
"llmTimeout": 5000
},
"rules": [
{
"id": "tdd-workflow",
"name": "Test-Driven Development",
"category": "dev-workflows",
"command": "superpowers:test-driven-development",
"enforcement": "suggest",
"keywords": ["test", "tdd", "failing test", "red green refactor"],
"patterns": ["\\b(write|add)\\s+tests?\\s+(first|before)\\b"],
"minMatches": 2,
"description": "TDD workflow with red-green-refactor cycle"
}
],
"directorySignals": [
{
"pattern": "src/components",
"boosts": { "ui": 2, "dev-workflows": 1 }
}
],
"fileTypeSignals": {
".tsx": { "ui": 2, "dev-workflows": 1 },
".py": { "data-science": 1 }
},
"skillSequences": {
"brainstorming": ["writing-plans"],
"writing-plans": ["executing-plans"]
},
"projectMarkers": [
{ "file": "package.json", "boosts": { "dev-workflows": 1 } },
{ "file": ".planning", "boosts": { "project-management": 2 } },
{ "absent": ".git", "penalties": { "git-workflows": -2 } }
]
}config section
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| maxMatches | number | 5 | Maximum number of matched skills returned per prompt. |
| minScore | number | 2 | Global minimum score threshold. Rules below this are excluded. |
| cacheTTL | number | 300000 | Cache time-to-live in milliseconds (5 minutes default). Repeated identical prompts in the same directory hit cache. |
| llmFallback | boolean | false | Enable Layer 2 LLM fallback when Layers 1+1.5 find nothing. |
| llmTimeout | number | 5000 | Timeout in milliseconds for Layer 2 LLM calls. |
rules array
Each rule object:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | yes | Unique kebab-case identifier (e.g., "tdd-workflow"). |
| name | string | yes | Human-readable display name. |
| category | string | yes | Grouping key for context signal boosts (e.g., "dev-workflows", "code-quality"). |
| command | string | yes | The skill command to invoke. Can be a custom command name ("tdd"), a namespaced skill ("superpowers:test-driven-development"), or any string your skill system recognizes. |
| enforcement | string | yes | One of "suggest", "silent", or "block". See Enforcement Levels. |
| keywords | string[] | yes | Word-boundary matches against the lowercased prompt. Each hit adds +1 to the score. |
| patterns | string[] | yes | Regular expressions tested against the raw prompt. Each hit adds +2 to the score. Use double-escaped backslashes in JSON ("\\b" for \b). |
| minMatches | number | no | Override the global minScore for this specific rule. If set, this rule uses its own threshold instead of config.minScore. |
| description | string | yes | Shown to the user when this rule matches. Keep it concise. |
Enforcement Levels
| Level | Behavior |
|-------|----------|
| suggest | Present to the user for confirmation before activating the skill. |
| silent | Mention the skill in context without requiring user action. |
| block | Require explicit acknowledgment from the user before proceeding. |
directorySignals (optional)
An array of directory pattern matchers. When the user's current working directory matches the pattern (tested as a regex), the specified categories receive score boosts.
{
"pattern": "src/components",
"boosts": { "ui": 2, "dev-workflows": 1 }
}If the user is working in /project/src/components/Button/, any rule in the "ui" category gets +2 and any rule in "dev-workflows" gets +1.
fileTypeSignals (optional)
An object mapping file extensions to category boosts. The router reads the current directory (up to 50 files) and counts extensions. If 3 or more files share an extension, the associated boosts are applied.
{
".tsx": { "ui": 2 },
".py": { "data-science": 1 }
}A directory full of .tsx files boosts "ui" rules by +2.
skillSequences (optional)
An object mapping skill commands to arrays of "next likely" skill commands. If the user recently ran a skill (within 2 hours), rules matching the next-in-sequence skills get a boost (+2 for the first item, +1 for subsequent).
{
"brainstorming": ["writing-plans"],
"writing-plans": ["executing-plans"]
}After a user runs a brainstorming skill, prompts that match "writing-plans" get a +2 sequence boost.
Session history is tracked per-process in a JSON file and resets when the Claude Code session changes.
projectMarkers (optional)
An array of file-existence checks. The router looks for marker files starting from the current directory and walking up to 5 parent directories.
[
{ "file": "package.json", "boosts": { "dev-workflows": 1 } },
{ "absent": ".git", "penalties": { "git-workflows": -2 } }
]Each marker has either:
file+boosts: if the file exists, apply boosts.absent+penalties: if the file does NOT exist, apply penalties.
Scoring
The router computes a score for each rule against the incoming prompt:
final_score = keyword_score + pattern_score + context_scoreKeyword score: For each keyword in the rule's
keywordsarray, if it matches as a whole word (word-boundary) in the lowercased prompt, add +1.Pattern score: For each regex in the rule's
patternsarray, if it matches the raw prompt (case-insensitive), add +2.Context score: Sum of all applicable context signal boosts and penalties for the rule's category:
- Directory signals: boost if cwd matches the pattern.
- File type signals: boost if the directory contains 3+ files of the matching extension.
- Project markers: boost if a marker file exists, penalize if an expected file is absent.
- Skill sequences: boost if the last skill used suggests this skill as a follow-up.
A rule is included in the results if its final_score >= minMatches (rule-level override) or final_score >= config.minScore (global default of 2).
Results are sorted by score descending, capped at config.maxMatches (default 5).
Scoring Example
Given a rule:
{
"id": "deployment",
"keywords": ["deploy", "release", "production"],
"patterns": ["\\bdeploy\\s+to\\s+(production|staging)\\b"],
"category": "dev-workflows",
"minMatches": 2
}And a prompt: "deploy to production please"
Keyword "deploy" -> found -> +1
Keyword "release" -> not found
Keyword "production" -> found -> +1
Pattern deploy\s+to -> found -> +2
---
Keyword score: 2
Pattern score: 2
Context score: 0 (no matching signals)
Final score: 4 (>= minMatches of 2 -> MATCH)Caching
The router caches results by a hash of prompt + cwd (prompts are truncated to 10,000 characters before hashing). Cached results expire after config.cacheTTL milliseconds (default: 5 minutes). This avoids redundant scoring when the same prompt appears multiple times in quick succession.
Skip conditions
The router returns no matches (short-circuits) when:
- The prompt is shorter than 10 characters.
- The prompt starts with
/(it is already a direct skill invocation).
Hook I/O
The hook reads from stdin and writes to stdout, following the Claude Code hooks contract.
Input (stdin):
{ "user_prompt": "deploy to production", "cwd": "/path/to/project" }Output when no match (stdout):
{}Output when matched (stdout):
{
"contextRouter": {
"matched": true,
"matchCount": 2,
"matches": [
{
"id": "deployment",
"name": "Deployment",
"command": "deploy",
"enforcement": "suggest",
"description": "Deploy, release, or ship code to environments",
"score": 5,
"layer1Score": 4,
"contextScore": 1,
"contextSignals": ["marker:+1"],
"layer": 1
}
],
"instruction": "Present these matched skills to the user for confirmation before activating."
}
}How to add a skill to route
If you already have a skill (a .claude/commands/*.md file or a plugin skill) and want the router to detect when it should be used:
Decide on keywords and patterns. Think about what a user would say when they want this skill. Pick 3-6 keywords (common words in relevant prompts) and 1-2 regex patterns (more specific phrase structures).
Run the interactive rule builder:
claude-dispatch add-ruleTest it:
claude-dispatch test "a prompt that should trigger your skill"Iterate. If the skill does not match, add more keywords or loosen the regex. If it matches too aggressively, raise
minMatchesor make patterns more specific. Runtestagain to verify.
Manual alternative: Edit .claude/dispatch-rules.json directly. Add a new object to the rules array following the schema, then run claude-dispatch validate to check for errors.
How to tell Claude agents to add rules
If you use Claude Code agents or automated workflows that create new skills, you can instruct them to also wire up routing rules. Add this to your project's CLAUDE.md:
## Dispatch Rules
When creating a new skill or command file in `.claude/commands/`, also add a routing
rule to `.claude/dispatch-rules.json` so the skill router can detect when users need it.
Rule schema (append to the `rules` array):
```json
{
"id": "kebab-case-id",
"name": "Human Readable Name",
"category": "category-name",
"command": "skill-command-name",
"enforcement": "suggest",
"keywords": ["keyword1", "keyword2", "keyword3"],
"patterns": ["\\bregex\\s+pattern\\b"],
"description": "One-line description of what the skill does"
}After editing, run claude-dispatch validate to confirm the config is valid.
This lets agents self-register their skills with the router without manual intervention.
## How to create a skill from scratch
A complete skill has two parts: the skill file itself and a dispatch rule to route to it.
### 1. Create the skill file
```bash
mkdir -p .claude/commandsCreate .claude/commands/my-skill.md:
---
name: my-skill
description: What this skill does in one line
---
# My Skill
Instructions for Claude when this skill is invoked.
## Steps
1. First, do this.
2. Then do that.
3. Finally, verify the result.2. Wire it to the router
claude-dispatch add-ruleOr manually add to .claude/dispatch-rules.json:
{
"id": "my-skill",
"name": "My Skill",
"category": "dev-workflows",
"command": "my-skill",
"enforcement": "suggest",
"keywords": ["relevant", "trigger", "words"],
"patterns": ["\\brelevant\\s+phrase\\b"],
"minMatches": 2,
"description": "One-line description shown when matched"
}3. Test it
claude-dispatch validate
claude-dispatch test "a prompt containing relevant trigger words"4. Use it
The next time a user sends a prompt that matches your keywords/patterns, the router will suggest the skill automatically.
Security & Limitations
See SECURITY-AUDIT.md for the full audit report.
Layer 2 LLM injection surface
When llmFallback is enabled, the user's prompt is interpolated into a classifier prompt sent to claude --print -m haiku. A crafted prompt could attempt to force-route to a specific rule. This is mitigated by: (1) Layer 2 is disabled by default, (2) output is filtered to known rule IDs only, and (3) suggest enforcement still requires user confirmation. If you enable Layer 2, be aware of this surface.
Layer 2 is intentionally available only in the standalone hook (templates/hook.js), not in the library API (src/router.js). It requires the claude CLI as a subprocess, making it unsuitable for general library consumption.
Regex pattern performance
Each rule's patterns are executed with a 100ms per-regex timeout (via vm.runInNewContext). With many rules and patterns, the aggregate timeout can add up: 10 rules × 3 patterns × 100ms = 3 seconds worst case. To keep routing fast:
- Keep pattern counts low (1-2 per rule is typical).
- Prefer keywords over patterns when possible — keywords use compiled word-boundary regexes that are much faster.
- Use
claude-dispatch testto profile matching latency on representative prompts. - The
isUnsafeRegexcheck catches most ReDoS patterns, but no heuristic is perfect. Avoid nested quantifiers and overlapping alternations.
Known limitations
| Limitation | Mitigation |
|------------|------------|
| TOCTOU race in symlink checks — Check-then-write has an inherent race window where a symlink could be created between the check and the write. | Standard practice; Node.js doesn't expose O_NOFOLLOW for writeFileSync. Risk requires local attacker with precise timing. |
| Read-write race in concurrent access — Simultaneous hook invocations could race on cache/history files. | Mitigated by atomic writes (tmp file + renameSync). Full file locking would require an external dependency, which is disproportionate for a CLI tool. |
| Directory listing in context signals — detectFileContext reads the cwd directory listing (filenames only, not contents) to count file extensions. | By design. Only reads the directory the user is already working in, limited to 50 entries, and only counts extensions — no file contents are accessed. |
Contributing
Contributions are welcome. Please follow these guidelines:
- Fork and branch. Create a feature branch from
main. - Write tests. New features need tests. Run the test suite with
npm test. - Follow existing patterns. Match the code style, naming conventions, and file organization already in the project.
- Keep changes focused. One feature or fix per pull request.
- Validate your rules. If you modify
templates/starter-rules.json, runclaude-dispatch validate -f templates/starter-rules.json.
Development setup
git clone https://github.com/wwadley-lucas/claude-dispatch.git
cd claude-dispatch
npm install
npm testRunning tests
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch