Files
ruflo/plugin/hooks/hooks.json
T
rUvandReuven d5fbb3bc4e fix(hooks): stop creating empty files via $TOOL_INPUT shell injection (#1747) (#1757)
The Liberation of Bajor team's report (#1747) is a precise diagnosis:
\`plugin/hooks/hooks.json\` interpolated \`\$TOOL_INPUT_command\` (and
\`\$TOOL_INPUT_*\`, \`\$PROMPT\`, \`\$TOOL_NAME\` etc.) directly into
double-quoted command strings. When the bash command being executed
contained both \" and >, the wrapping quotes broke and the shell
re-parsed the inner content. Any > became a redirect, creating empty
files at CWD with names like '3', '40)', 'maxWidth)', etc.

After ~24h of normal usage on Windows + Git Bash, dozens of empty
files accumulated at every project root.

Fix: rewrite ALL 13 command hooks to use the stdin-jq-xargs pattern
that the companion file \`.claude-plugin/hooks/hooks.json\` already
uses safely:

    cat | jq -r '.tool_input.command // empty' | tr '\\\\n' '\\\\0' \\
        | xargs -0 -I {} npx claude-flow@alpha hooks pre-command --command '{}'

Reading via stdin + jq + xargs -0 -I {} bypasses shell re-parsing
entirely. The user input becomes a single argv element, never
re-interpreted as shell tokens.

Validation (script in commit):
  safe (stdin-jq): 13   unsafe (\$-interp): 0

Affected hooks (all rewritten):
  PreToolUse:  Bash, Write|Edit|MultiEdit, Task, Grep|Glob|Read,
               mcp__claude-flow__*
  PostToolUse: same 5 matchers
  UserPromptSubmit: route by .prompt
  SessionStart:    by .session_id
  Notification:    by .message

Dropped non-essential \$TOOL_SUCCESS / \$TOOL_EXIT_CODE flags from
post-* hooks — they're internal booleans/numbers (not user input,
so not the file-creation vector) but the cleaner stdin-only pattern
is preferable over mixed inline/stdin.

Stop / SubagentStop / PermissionRequest hooks unchanged — they don't
interpolate user input.

This is a plugin-file change; no npm publish needed. The fix is live
when the PR merges and users pull main / re-clone.

Closes #1747.

Co-authored-by: Reuven <cohen@ruv-mac-mini.local>
2026-05-05 10:43:52 -04:00

225 lines
8.3 KiB
JSON

{
"$schema": "https://code.claude.com/schemas/hooks.json",
"description": "Claude Flow hooks configuration — uses stdin-jq-xargs pattern to prevent shell-injection when tool inputs contain quotes / redirects / special chars (#1747).",
"_security_note": "All commands read the hook payload from stdin (Claude Code passes a JSON object), extract fields with jq, and pass them to npx as a single argv element via xargs -0. This bypasses shell re-parsing entirely. DO NOT inline $TOOL_INPUT_* / $PROMPT / $TOOL_NAME directly in a quoted command string — interpolation is not shell-safe (creates empty files at CWD when input contains '>' redirects).",
"hooks": {
"PreToolUse": [
{
"matcher": "^(Write|Edit|MultiEdit)$",
"description": "Pre-edit hook for file modifications",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '.tool_input.file_path // .tool_input.path // empty' | tr '\\n' '\\0' | xargs -0 -I {} npx claude-flow@alpha hooks pre-edit --file '{}'",
"timeout": 5000,
"continueOnError": true
}
]
},
{
"matcher": "^Bash$",
"description": "Pre-command hook for bash execution",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '.tool_input.command // empty' | tr '\\n' '\\0' | xargs -0 -I {} npx claude-flow@alpha hooks pre-command --command '{}'",
"timeout": 3000,
"continueOnError": true
}
]
},
{
"matcher": "^Task$",
"description": "Pre-task hook for agent spawning",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '.tool_input.description // empty | .[:200]' | tr '\\n' '\\0' | xargs -0 -I {} npx claude-flow@alpha hooks pre-task --description '{}'",
"timeout": 5000,
"continueOnError": true
}
]
},
{
"matcher": "^(Grep|Glob|Read)$",
"description": "Pre-search hook for caching",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '.tool_input.pattern // .tool_input.query // empty' | tr '\\n' '\\0' | xargs -0 -I {} npx claude-flow@alpha hooks pre-search --query '{}'",
"timeout": 2000,
"continueOnError": true
}
]
},
{
"matcher": "^mcp__claude-flow__.*$",
"description": "Pre-MCP hook for swarm coordination",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '.tool_name // empty' | tr '\\n' '\\0' | xargs -0 -I {} npx claude-flow@alpha hooks mcp-pre --tool '{}'",
"timeout": 3000,
"continueOnError": true
}
]
}
],
"PostToolUse": [
{
"matcher": "^(Write|Edit|MultiEdit)$",
"description": "Post-edit hook for formatting and learning",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '.tool_input.file_path // .tool_input.path // empty' | tr '\\n' '\\0' | xargs -0 -I {} npx claude-flow@alpha hooks post-edit --file '{}' --train-patterns",
"timeout": 5000,
"continueOnError": true
}
]
},
{
"matcher": "^Bash$",
"description": "Post-command hook for metrics",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '.tool_input.command // empty' | tr '\\n' '\\0' | xargs -0 -I {} npx claude-flow@alpha hooks post-command --command '{}' --track-metrics true --store-results true",
"timeout": 3000,
"continueOnError": true
}
]
},
{
"matcher": "^Task$",
"description": "Post-task hook for performance analysis",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '.tool_response.agent_id // .tool_response.task_id // empty' | tr '\\n' '\\0' | xargs -0 -I {} npx claude-flow@alpha hooks post-task --task-id '{}' --analyze-performance",
"timeout": 5000,
"continueOnError": true
}
]
},
{
"matcher": "^(Grep|Glob|Read)$",
"description": "Post-search hook for caching results",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '.tool_input.pattern // .tool_input.query // empty' | tr '\\n' '\\0' | xargs -0 -I {} npx claude-flow@alpha hooks post-search --query '{}' --cache-results",
"timeout": 2000,
"continueOnError": true
}
]
},
{
"matcher": "^mcp__claude-flow__.*$",
"description": "Post-MCP hook for coordination state",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '.tool_name // empty' | tr '\\n' '\\0' | xargs -0 -I {} npx claude-flow@alpha hooks mcp-post --tool '{}'",
"timeout": 3000,
"continueOnError": true
}
]
}
],
"UserPromptSubmit": [
{
"description": "Route tasks to optimal agents",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '.prompt // empty' | tr '\\n' '\\0' | xargs -0 -I {} npx claude-flow@alpha hooks route --task '{}' --include-explanation",
"timeout": 5000,
"continueOnError": true
}
]
}
],
"SessionStart": [
{
"description": "Initialize session and restore context",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '.session_id // empty' | tr '\\n' '\\0' | xargs -0 -I {} npx claude-flow@alpha hooks session-start --session-id '{}' --load-context",
"timeout": 10000,
"continueOnError": true
}
]
}
],
"Stop": [
{
"description": "Evaluate if task is complete",
"hooks": [
{
"type": "prompt",
"prompt": "Evaluate if the current task has been completed successfully. Consider: 1) Were all requested changes made? 2) Did tests pass? 3) Is there follow-up work needed? Respond with {\"decision\": \"stop\"} if complete, or {\"decision\": \"continue\", \"reason\": \"...\"} if more work is needed."
}
]
}
],
"SubagentStop": [
{
"description": "Evaluate if subagent task is complete",
"hooks": [
{
"type": "prompt",
"prompt": "Evaluate if the subagent has completed its assigned task. Check if the work output meets the requirements. Respond with {\"decision\": \"stop\"} if complete, or {\"decision\": \"continue\", \"reason\": \"...\"} if more work is needed."
}
]
}
],
"Notification": [
{
"description": "Handle notifications with swarm status",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '.message // empty' | tr '\\n' '\\0' | xargs -0 -I {} npx claude-flow@alpha hooks notify --message '{}' --swarm-status",
"timeout": 3000,
"continueOnError": true
}
]
}
],
"PermissionRequest": [
{
"matcher": "^mcp__claude-flow__.*$",
"description": "Auto-allow claude-flow MCP tools",
"hooks": [
{
"type": "command",
"command": "echo '{\"decision\": \"allow\", \"reason\": \"claude-flow MCP tool auto-approved\"}'",
"timeout": 1000
}
]
}
]
},
"v3HookMapping": {
"_comment": "Maps V3 internal hook events to official Claude Code hooks",
"PreToolUse": "PreToolUse",
"PostToolUse": "PostToolUse",
"PreEdit": "PreToolUse (matcher: Edit|Write)",
"PostEdit": "PostToolUse (matcher: Edit|Write)",
"PreCommand": "PreToolUse (matcher: Bash)",
"PostCommand": "PostToolUse (matcher: Bash)",
"PreTask": "UserPromptSubmit",
"PostTask": "PostToolUse (matcher: Task)",
"SessionStart": "SessionStart",
"SessionEnd": "Stop",
"AgentSpawn": "PostToolUse (matcher: Task)",
"AgentTerminate": "SubagentStop",
"PreRoute": "UserPromptSubmit",
"PostRoute": "PostToolUse",
"PatternLearned": "PostToolUse (internal)",
"PatternConsolidated": "Stop (internal)"
}
}