diff --git a/v3/@claude-flow/cli/.claude/hooks/hook-bridge.sh b/v3/@claude-flow/cli/.claude/hooks/hook-bridge.sh new file mode 100755 index 0000000000..261b405317 --- /dev/null +++ b/v3/@claude-flow/cli/.claude/hooks/hook-bridge.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# hook-bridge.sh - Bridges Claude Code hooks (stdin JSON) to claude-flow CLI +# +# Claude Code passes hook context as JSON via stdin, NOT as shell environment +# variables. This script reads the stdin JSON, extracts the relevant fields +# using jq, and forwards them as proper CLI arguments. +# +# Usage: hook-bridge.sh +# +# Requires: jq (https://jqlang.github.io/jq/) + +MODE="${1:-}" + +# Read JSON from stdin (Claude Code always pipes hook data this way) +INPUT=$(cat 2>/dev/null) || INPUT='{}' +[ -z "$INPUT" ] && INPUT='{}' + +# Safe JSON field extraction - returns empty string on missing/null fields +jf() { + echo "$INPUT" | jq -r "$1" 2>/dev/null | grep -v '^null$' || echo "" +} + +case "$MODE" in + # -- PreToolUse hooks ----------------------------------------------- + pre-edit) + FILE=$(jf '.tool_input.file_path') + [ -z "$FILE" ] && exit 0 + exec npx @claude-flow/cli@latest hooks pre-edit --file "$FILE" + ;; + + pre-command) + CMD=$(jf '.tool_input.command') + [ -z "$CMD" ] && exit 0 + exec npx @claude-flow/cli@latest hooks pre-command --command "$CMD" + ;; + + pre-task) + DESC=$(jf '.tool_input.prompt // .tool_input.description') + [ -z "$DESC" ] && exit 0 + exec npx @claude-flow/cli@latest hooks pre-task --description "$DESC" + ;; + + # -- PostToolUse hooks ---------------------------------------------- + post-edit) + FILE=$(jf '.tool_input.file_path') + [ -z "$FILE" ] && exit 0 + exec npx @claude-flow/cli@latest hooks post-edit --file "$FILE" --success true + ;; + + post-command) + CMD=$(jf '.tool_input.command') + [ -z "$CMD" ] && exit 0 + exec npx @claude-flow/cli@latest hooks post-command --command "$CMD" --success true + ;; + + post-task) + AGENT_ID=$(jf '.tool_input.description // .tool_input.prompt') + [ -z "$AGENT_ID" ] && AGENT_ID="unknown" + exec npx @claude-flow/cli@latest hooks post-task --task-id "$AGENT_ID" --success true + ;; + + # -- UserPromptSubmit ----------------------------------------------- + route) + PROMPT=$(jf '.prompt') + [ -z "$PROMPT" ] && exit 0 + exec npx @claude-flow/cli@latest hooks route --task "$PROMPT" + ;; + + # -- SessionStart --------------------------------------------------- + daemon-start) + exec npx @claude-flow/cli@latest daemon start --quiet + ;; + + session-restore) + SID=$(jf '.session_id') + if [ -n "$SID" ]; then + exec npx @claude-flow/cli@latest hooks session-restore --session-id "$SID" + else + exec npx @claude-flow/cli@latest hooks session-restore --latest + fi + ;; + + # -- Stop ----------------------------------------------------------- + stop-check) + echo '{"ok":true}' + exit 0 + ;; + + # -- Notification --------------------------------------------------- + notify) + MSG=$(jf '.message // .notification_message // .content') + [ -z "$MSG" ] && exit 0 + exec npx @claude-flow/cli@latest memory store --namespace notifications --key "notify" --value "$MSG" + ;; + + *) + echo "Unknown hook mode: $MODE" >&2 + exit 1 + ;; +esac diff --git a/v3/@claude-flow/cli/.claude/settings.json b/v3/@claude-flow/cli/.claude/settings.json index bfbfada31f..8c6127f00c 100644 --- a/v3/@claude-flow/cli/.claude/settings.json +++ b/v3/@claude-flow/cli/.claude/settings.json @@ -6,8 +6,8 @@ "hooks": [ { "type": "command", - "command": "[ -n \"$TOOL_INPUT_file_path\" ] && npx @claude-flow/cli@latest hooks pre-edit --file \"$TOOL_INPUT_file_path\" 2>/dev/null || true", - "timeout": 5000, + "command": ".claude/hooks/hook-bridge.sh pre-edit", + "timeout": 10000, "continueOnError": true } ] @@ -17,8 +17,8 @@ "hooks": [ { "type": "command", - "command": "[ -n \"$TOOL_INPUT_command\" ] && npx @claude-flow/cli@latest hooks pre-command --command \"$TOOL_INPUT_command\" 2>/dev/null || true", - "timeout": 5000, + "command": ".claude/hooks/hook-bridge.sh pre-command", + "timeout": 10000, "continueOnError": true } ] @@ -28,8 +28,8 @@ "hooks": [ { "type": "command", - "command": "[ -n \"$TOOL_INPUT_prompt\" ] && npx @claude-flow/cli@latest hooks pre-task --task-id \"task-$(date +%s)\" --description \"$TOOL_INPUT_prompt\" 2>/dev/null || true", - "timeout": 5000, + "command": ".claude/hooks/hook-bridge.sh pre-task", + "timeout": 10000, "continueOnError": true } ] @@ -41,8 +41,8 @@ "hooks": [ { "type": "command", - "command": "[ -n \"$TOOL_INPUT_file_path\" ] && npx @claude-flow/cli@latest hooks post-edit --file \"$TOOL_INPUT_file_path\" --success \"${TOOL_SUCCESS:-true}\" 2>/dev/null || true", - "timeout": 5000, + "command": ".claude/hooks/hook-bridge.sh post-edit", + "timeout": 10000, "continueOnError": true } ] @@ -52,8 +52,8 @@ "hooks": [ { "type": "command", - "command": "[ -n \"$TOOL_INPUT_command\" ] && npx @claude-flow/cli@latest hooks post-command --command \"$TOOL_INPUT_command\" --success \"${TOOL_SUCCESS:-true}\" 2>/dev/null || true", - "timeout": 5000, + "command": ".claude/hooks/hook-bridge.sh post-command", + "timeout": 10000, "continueOnError": true } ] @@ -63,8 +63,8 @@ "hooks": [ { "type": "command", - "command": "[ -n \"$TOOL_RESULT_agent_id\" ] && npx @claude-flow/cli@latest hooks post-task --task-id \"$TOOL_RESULT_agent_id\" --success \"${TOOL_SUCCESS:-true}\" 2>/dev/null || true", - "timeout": 5000, + "command": ".claude/hooks/hook-bridge.sh post-task", + "timeout": 10000, "continueOnError": true } ] @@ -75,8 +75,8 @@ "hooks": [ { "type": "command", - "command": "[ -n \"$PROMPT\" ] && npx @claude-flow/cli@latest hooks route --task \"$PROMPT\" || true", - "timeout": 5000, + "command": ".claude/hooks/hook-bridge.sh route", + "timeout": 10000, "continueOnError": true } ] @@ -87,14 +87,14 @@ "hooks": [ { "type": "command", - "command": "npx @claude-flow/cli@latest daemon start --quiet 2>/dev/null || true", - "timeout": 5000, + "command": ".claude/hooks/hook-bridge.sh daemon-start", + "timeout": 10000, "continueOnError": true }, { "type": "command", - "command": "[ -n \"$SESSION_ID\" ] && npx @claude-flow/cli@latest hooks session-restore --session-id \"$SESSION_ID\" 2>/dev/null || true", - "timeout": 10000, + "command": ".claude/hooks/hook-bridge.sh session-restore", + "timeout": 15000, "continueOnError": true } ] @@ -105,7 +105,7 @@ "hooks": [ { "type": "command", - "command": "echo '{\"ok\": true}'", + "command": ".claude/hooks/hook-bridge.sh stop-check", "timeout": 1000 } ] @@ -116,8 +116,8 @@ "hooks": [ { "type": "command", - "command": "[ -n \"$NOTIFICATION_MESSAGE\" ] && npx @claude-flow/cli@latest memory store --namespace notifications --key \"notify-$(date +%s)\" --value \"$NOTIFICATION_MESSAGE\" 2>/dev/null || true", - "timeout": 3000, + "command": ".claude/hooks/hook-bridge.sh notify", + "timeout": 5000, "continueOnError": true } ] @@ -234,4 +234,4 @@ "threatModel": true } } -} \ No newline at end of file +} diff --git a/v3/@claude-flow/cli/src/init/executor.ts b/v3/@claude-flow/cli/src/init/executor.ts index 5cde164f74..33d2c3ffdb 100644 --- a/v3/@claude-flow/cli/src/init/executor.ts +++ b/v3/@claude-flow/cli/src/init/executor.ts @@ -129,6 +129,7 @@ const DIRECTORIES = { '.claude/commands', '.claude/agents', '.claude/helpers', + '.claude/hooks', ], runtime: [ '.claude-flow', @@ -757,6 +758,57 @@ function findSourceHelpersDir(sourceBaseDir?: string): string | null { return null; } +/** + * Write hook-bridge.sh to .claude/hooks/ + * + * This script is required for Claude Code hooks to function correctly. + * Claude Code passes hook context as JSON via stdin (not as env vars), + * so a bridge script is needed to parse the JSON and forward fields + * as proper CLI arguments. + */ +async function writeHookBridge( + targetDir: string, + options: InitOptions, + result: InitResult +): Promise { + const hooksDir = path.join(targetDir, '.claude', 'hooks'); + + if (!fs.existsSync(hooksDir)) { + fs.mkdirSync(hooksDir, { recursive: true }); + } + + const bridgePath = path.join(hooksDir, 'hook-bridge.sh'); + + if (fs.existsSync(bridgePath) && !options.force) { + result.skipped.push('.claude/hooks/hook-bridge.sh'); + return; + } + + // Try to copy from package source first + const sourceHooksDir = findSourceDir('hooks', options.sourceBaseDir); + if (sourceHooksDir) { + const sourceBridge = path.join(sourceHooksDir, 'hook-bridge.sh'); + if (fs.existsSync(sourceBridge)) { + fs.copyFileSync(sourceBridge, bridgePath); + fs.chmodSync(bridgePath, '755'); + result.created.files.push('.claude/hooks/hook-bridge.sh'); + return; + } + } + + // Fallback: check package's own .claude/hooks directory + const packageRoot = path.resolve(__dirname, '..', '..', '..'); + const packageBridge = path.join(packageRoot, '.claude', 'hooks', 'hook-bridge.sh'); + if (fs.existsSync(packageBridge)) { + fs.copyFileSync(packageBridge, bridgePath); + fs.chmodSync(bridgePath, '755'); + result.created.files.push('.claude/hooks/hook-bridge.sh'); + return; + } + + result.errors.push('Could not find hook-bridge.sh source. Hooks may not function correctly.'); +} + /** * Write helper scripts */ @@ -767,6 +819,11 @@ async function writeHelpers( ): Promise { const helpersDir = path.join(targetDir, '.claude', 'helpers'); + // Write hook-bridge.sh (required for Claude Code hooks to work) + // Claude Code passes hook context as JSON via stdin, not as env vars. + // hook-bridge.sh reads the JSON and forwards fields as CLI arguments. + await writeHookBridge(targetDir, options, result); + // Find source helpers directory (works for npm package and local dev) const sourceHelpersDir = findSourceHelpersDir(options.sourceBaseDir); diff --git a/v3/@claude-flow/cli/src/init/settings-generator.ts b/v3/@claude-flow/cli/src/init/settings-generator.ts index c355d9254f..50110ba311 100644 --- a/v3/@claude-flow/cli/src/init/settings-generator.ts +++ b/v3/@claude-flow/cli/src/init/settings-generator.ts @@ -143,11 +143,17 @@ function generateStatusLineConfig(options: InitOptions): object { /** * Generate hooks configuration + * + * IMPORTANT: Claude Code passes hook context as JSON via stdin, NOT as shell + * environment variables. The hook-bridge.sh script reads stdin JSON with jq + * and forwards the extracted fields as proper CLI arguments. + * + * See .claude/hooks/hook-bridge.sh for the implementation. */ function generateHooksConfig(config: HooksConfig): object { const hooks: Record = {}; - // PreToolUse hooks - cross-platform via npx with defensive guards + // PreToolUse hooks - use hook-bridge.sh to parse stdin JSON if (config.preToolUse) { hooks.PreToolUse = [ // File edit hooks with intelligence routing @@ -156,7 +162,7 @@ function generateHooksConfig(config: HooksConfig): object { hooks: [ { type: 'command', - command: '[ -n "$TOOL_INPUT_file_path" ] && npx @claude-flow/cli@latest hooks pre-edit --file "$TOOL_INPUT_file_path" 2>/dev/null || true', + command: '.claude/hooks/hook-bridge.sh pre-edit', timeout: config.timeout, continueOnError: true, }, @@ -168,19 +174,19 @@ function generateHooksConfig(config: HooksConfig): object { hooks: [ { type: 'command', - command: '[ -n "$TOOL_INPUT_command" ] && npx @claude-flow/cli@latest hooks pre-command --command "$TOOL_INPUT_command" 2>/dev/null || true', + command: '.claude/hooks/hook-bridge.sh pre-command', timeout: config.timeout, continueOnError: true, }, ], }, - // Task/Agent hooks - require task-id for tracking + // Task/Agent hooks { matcher: '^Task$', hooks: [ { type: 'command', - command: '[ -n "$TOOL_INPUT_prompt" ] && npx @claude-flow/cli@latest hooks pre-task --task-id "task-$(date +%s)" --description "$TOOL_INPUT_prompt" 2>/dev/null || true', + command: '.claude/hooks/hook-bridge.sh pre-task', timeout: config.timeout, continueOnError: true, }, @@ -189,7 +195,7 @@ function generateHooksConfig(config: HooksConfig): object { ]; } - // PostToolUse hooks - cross-platform via npx with defensive guards + // PostToolUse hooks - use hook-bridge.sh to parse stdin JSON if (config.postToolUse) { hooks.PostToolUse = [ // File edit hooks with neural pattern training @@ -198,7 +204,7 @@ function generateHooksConfig(config: HooksConfig): object { hooks: [ { type: 'command', - command: '[ -n "$TOOL_INPUT_file_path" ] && npx @claude-flow/cli@latest hooks post-edit --file "$TOOL_INPUT_file_path" --success "${TOOL_SUCCESS:-true}" 2>/dev/null || true', + command: '.claude/hooks/hook-bridge.sh post-edit', timeout: config.timeout, continueOnError: true, }, @@ -210,19 +216,19 @@ function generateHooksConfig(config: HooksConfig): object { hooks: [ { type: 'command', - command: '[ -n "$TOOL_INPUT_command" ] && npx @claude-flow/cli@latest hooks post-command --command "$TOOL_INPUT_command" --success "${TOOL_SUCCESS:-true}" 2>/dev/null || true', + command: '.claude/hooks/hook-bridge.sh post-command', timeout: config.timeout, continueOnError: true, }, ], }, - // Task completion hooks - use task-id + // Task completion hooks { matcher: '^Task$', hooks: [ { type: 'command', - command: '[ -n "$TOOL_RESULT_agent_id" ] && npx @claude-flow/cli@latest hooks post-task --task-id "$TOOL_RESULT_agent_id" --success "${TOOL_SUCCESS:-true}" 2>/dev/null || true', + command: '.claude/hooks/hook-bridge.sh post-task', timeout: config.timeout, continueOnError: true, }, @@ -238,7 +244,7 @@ function generateHooksConfig(config: HooksConfig): object { hooks: [ { type: 'command', - command: '[ -n "$PROMPT" ] && npx @claude-flow/cli@latest hooks route --task "$PROMPT" || true', + command: '.claude/hooks/hook-bridge.sh route', timeout: config.timeout, continueOnError: true, }, @@ -254,14 +260,14 @@ function generateHooksConfig(config: HooksConfig): object { hooks: [ { type: 'command', - command: 'npx @claude-flow/cli@latest daemon start --quiet 2>/dev/null || true', - timeout: 5000, + command: '.claude/hooks/hook-bridge.sh daemon-start', + timeout: 10000, continueOnError: true, }, { type: 'command', - command: '[ -n "$SESSION_ID" ] && npx @claude-flow/cli@latest hooks session-restore --session-id "$SESSION_ID" 2>/dev/null || true', - timeout: 10000, + command: '.claude/hooks/hook-bridge.sh session-restore', + timeout: 15000, continueOnError: true, }, ], @@ -270,14 +276,13 @@ function generateHooksConfig(config: HooksConfig): object { } // Stop hooks for task evaluation - always return ok by default - // The hook outputs JSON that Claude Code validates if (config.stop) { hooks.Stop = [ { hooks: [ { type: 'command', - command: 'echo \'{"ok": true}\'', + command: '.claude/hooks/hook-bridge.sh stop-check', timeout: 1000, }, ], @@ -292,8 +297,8 @@ function generateHooksConfig(config: HooksConfig): object { hooks: [ { type: 'command', - command: '[ -n "$NOTIFICATION_MESSAGE" ] && npx @claude-flow/cli@latest memory store --namespace notifications --key "notify-$(date +%s)" --value "$NOTIFICATION_MESSAGE" 2>/dev/null || true', - timeout: 3000, + command: '.claude/hooks/hook-bridge.sh notify', + timeout: 5000, continueOnError: true, }, ],