Files
ruflo/plugin/hooks/hooks.json
T
rUvandReuven f0ee0f9688 fix(#1921): plugin hooks use a resilient shim, not bare npx @alpha per fire (#1923)
* fix(#1921): plugin hooks invoke a resilient shim, not bare `npx @alpha` per fire

Every PreToolUse/PostToolUse/Stop hook ran `npx <pkg>@alpha hooks …`, which
(a) re-resolves the @alpha dist-tag from the registry on every fire and
(b) re-installs from cold cache — and when that install crashes (an arborist
`Invalid Version` on npm 10.8.x, deep in a transitive OTEL/grpc subtree) the
user sees a hook error in Claude Code after every assistant turn, plus ~7s of
wasted registry traffic per turn.

Fix: each plugin (`.claude-plugin/`, `plugin/`, `plugins/ruflo-core/`) ships
`scripts/ruflo-hook.sh` — prefers an already-installed `ruflo`/`claude-flow`
binary, falls back to `npx --prefer-offline --yes ruflo@alpha`, and ALWAYS
exits 0. hooks.json invokes `"${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh" …
|| true` (the `|| true` covers the unset-$CLAUDE_PLUGIN_ROOT case). stdin
(the hook event JSON) passes through unchanged; the stdin-jq-xargs
shell-injection-safety pattern in plugin/hooks/hooks.json is preserved.

This does NOT fix the underlying arborist crash (which needs the offending
empty-`version` package pinned via `overrides` once it's identified on
npm 10.8.2 — see the issue thread) — it makes a CLI/install failure invisible
and cheap instead of a visible error every turn.

CI guard: scripts/audit-hook-commands.mjs — fails CI if any hook `command`
uses `npx` without `--prefer-offline`, or invokes the CLI without a non-fatal
guard. Wired into v3-ci.yml as `hook-command-audit` (also bash-lints each
ruflo-hook.sh and asserts it exits 0 with no CLI on PATH) + added to
witness-verify needs[]. Path triggers updated to fire on `**/hooks/hooks.json`,
`**/scripts/ruflo-hook.sh`, and `scripts/**`. Witness markers for #1921 +

Co-Authored-By: RuFlo <ruv@ruv.net>

* fix(#1921): make test-hooks.mjs shim-aware; fix shim-lint CI step; regen witness

Rebasing #1921 onto main surfaced 3 CI failures:

- plugin-hooks-smoke (ubuntu + macos): test-hooks.mjs substituted only
  `npx ruflo@alpha` → the local CLI, but the hooks.json now invokes
  `${CLAUDE_PLUGIN_ROOT}/scripts/ruflo-hook.sh <args>`. It now also
  substitutes the shim path → `<cli> hooks <args>` (bypassing the shim, so
  the test exercises the real CLI flag wiring) and strips the shim's
  trailing `|| true` so exit codes are still asserted. 7/7 pass.
- hook-command-audit shim-lint step: `PATH="/nonexistent" bash "$sh"` set
  PATH to a dir with no `bash`, so `bash: command not found`. Now invokes
  bash by full path with `PATH=` empty: `PATH= "$BASH" "$sh"` — the shim's
  `command -v` is a builtin (works with empty PATH) and the npx fallback
  fails cleanly under `|| true` → exit 0. (resolved in v3-ci.yml during the
  rebase.)

Witness manifest regenerated; #1862's marker refreshed to match the
shim-relocated `post-edit -f "$FILE" -s true` (the documented-flag form is
preserved). `ruflo verify` 0 regressed.

Co-Authored-By: RuFlo <ruv@ruv.net>

---------

Co-authored-by: Reuven <cohen@ruv-mac-mini.local>
2026-05-12 00:51:18 -04:00

226 lines
9.1 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.",
"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)"
}
}