Files
ruvnet b68ad4ccba fix(plugins): make ruflo-core/ruflo-cost-tracker hooks Windows-native (#2721)
Both plugins' hooks.json wrapped every command in `/bin/bash -c '...'`,
which fails outright on native Windows (no such path) -- Codex/Claude
Code report "PreToolUse hook (failed) -- exit code 1" on every tool
call. The `_platform: posix` / "ruflo init overrides this on Windows"
claim in both files was never actually true: Claude Code merges
plugin-declared hooks additively with any init-generated
.claude/settings.json, it doesn't replace them, and there's no `ruflo
init` step at all in the reported Codex marketplace install flow.

Fix: every hook command is now a `node -e` bootstrap that resolves
plugins/*/scripts/ruflo-hook.cjs from process.env.CLAUDE_PLUGIN_ROOT
inside Node -- no shell env-var expansion (${VAR} vs %VAR%), so the
exact same command string runs unchanged on Windows/macOS/Linux.

ruflo-core's ruflo-hook.cjs (previously a full port of ruflo-hook.sh
that existed on disk but was never referenced by hooks.json) gained:
  - JSON parsing of the hook event from stdin (replaces jq) for
    post-command/post-edit, deriving the same CLI flags the bash
    version computed
  - the PreToolUse permission-allow stdout echo Cursor's stricter
    contract requires (previously only the bash wrapper's trailing
    printf did this)
  - precompact-manual/precompact-auto guidance text (previously plain
    bash echoes, no CLI call)
  - a real Windows shell-quoting fix: shell:true with an args array
    does NOT quote array elements, so "echo hi" silently truncated to
    "echo" and a heredoc's `<<` errored as unexpected -- skip the
    shell entirely for `node` invocations (never a .cmd shim, so
    CreateProcess gets the argv array byte-for-byte)

cost-tracker's existing ruflo-hook.cjs (already correct, just
orphaned) needed no logic changes, only wiring.

Also:
  - corrected the false "_platform_note" claims about ruflo init
    overriding plugin hooks
  - hardened scripts/audit-plugin-hooks-cross-platform.mjs: a
    POSIX-exempt hooks.json now must actually reference its sibling
    .cjs shim, not just have one sitting on disk unreferenced (which
    is exactly the shape cost-tracker shipped in undetected)
  - added windows-latest to the plugin-hooks-smoke CI matrix (it was
    ubuntu/macos-only because the old bash-based hooks.json couldn't
    run on Windows at all) and rewrote test-hooks.mjs to drive hooks.json's
    literal command strings via `shell: true` -- exactly how Claude
    Code/Codex invoke them -- instead of wrapping everything in an
    explicit `bash -c` that could never have caught this bug
  - flagged (not fixed) a separate, currently-published, actively
    maintained plugin package (.claude-plugin/ + plugin/, the older
    "claude-flow" plugin, not listed in the ruflo marketplace) with
    the same underlying bug via jq/xargs pipes instead of bash --
    explicitly marked _legacy_unaudited_shim so the hardened audit
    doesn't silently regress on out-of-scope work

Verified locally on native Windows (this fix's actual target
platform): all 17 ruflo-core hook cases pass, all 3 cost-tracker
cases pass, the existing 12-case smoke-ruflo-hook-cjs.mjs passes
unchanged, both hook-command audits pass clean.

Fixes #2721
2026-07-18 19:05:06 -04:00

230 lines
10 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). Hook subcommands run via scripts/ruflo-hook.sh (#1921).",
"_security_note": "All commands read the hook payload from stdin (Claude Code passes a JSON object), extract fields with jq, and pass them to the CLI 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).",
"_resilience_note": "#1921 — hook subcommands invoke scripts/ruflo-hook.sh (resilient shim): prefers a locally-installed `ruflo`/`claude-flow` binary, falls back to `npx --prefer-offline`, always exits 0. The trailing `|| true` on each pipeline guards the case where $CLAUDE_PLUGIN_ROOT is unset. DO NOT revert to a bare `npx <pkg>@alpha hooks …` per fire.",
"_platform": "posix",
"_platform_note": "#2132 — This hooks.json uses /bin/bash, POSIX pipelines (jq, xargs, tr), and .sh scripts. It is intentionally POSIX-only (Mac/Linux) today and known-broken on native Windows (#2721 shape). The previous claim here that `ruflo init` overrides these entries with node-based equivalents on Windows was never actually implemented and is incorrect — Claude Code merges plugin-declared hooks additively with any init-generated settings.json, it does not replace them. This package (separate from plugins/ruflo-core, which got the #2721 fix) still needs its own Windows-compatible rewrite; see _legacy_unaudited_shim below.",
"_legacy_unaudited_shim": true,
"_legacy_unaudited_shim_note": "#2721 fixed plugins/ruflo-core and plugins/ruflo-cost-tracker (marketplace-listed in .claude-plugin/marketplace.json) by rewiring hooks.json to a `node -e` bootstrap around scripts/ruflo-hook.cjs. This older, separately-published \"claude-flow\" plugin package (not in the ruflo marketplace list) has its own, larger jq/xargs-based hook set and its own scripts/ruflo-hook.cjs that hooks.json never references — same underlying bug shape, NOT fixed as part of #2721. Needs its own audited pass before this flag can be removed.",
"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 {} \"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh\" pre-edit --file '{}' || true",
"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 {} \"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh\" pre-command --command '{}' || true",
"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 {} \"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh\" pre-task --description '{}' || true",
"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 {} \"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh\" pre-search --query '{}' || true",
"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 {} \"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh\" mcp-pre --tool '{}' || true",
"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 {} \"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh\" post-edit --file '{}' --train-patterns || true",
"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 {} \"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh\" post-command --command '{}' --track-metrics true --store-results true || 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 {} \"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh\" post-task --task-id '{}' --analyze-performance || true",
"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 {} \"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh\" post-search --query '{}' --cache-results || true",
"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 {} \"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh\" mcp-post --tool '{}' || true",
"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 {} \"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh\" route --task '{}' --include-explanation || true",
"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 {} \"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh\" session-start --session-id '{}' --load-context || true",
"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 {} \"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh\" notify --message '{}' --swarm-status || true",
"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)"
}
}