Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions v3/@claude-flow/cli/.claude/hooks/hook-bridge.sh
Original file line number Diff line number Diff line change
@@ -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 <mode>
#
# 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
44 changes: 22 additions & 22 deletions v3/@claude-flow/cli/.claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand All @@ -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
}
]
Expand All @@ -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
}
]
Expand All @@ -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
}
]
Expand All @@ -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
}
]
Expand All @@ -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
}
]
Expand All @@ -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
}
]
Expand All @@ -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
}
]
Expand All @@ -105,7 +105,7 @@
"hooks": [
{
"type": "command",
"command": "echo '{\"ok\": true}'",
"command": ".claude/hooks/hook-bridge.sh stop-check",
"timeout": 1000
}
]
Expand All @@ -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
}
]
Expand Down Expand Up @@ -234,4 +234,4 @@
"threatModel": true
}
}
}
}
57 changes: 57 additions & 0 deletions v3/@claude-flow/cli/src/init/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const DIRECTORIES = {
'.claude/commands',
'.claude/agents',
'.claude/helpers',
'.claude/hooks',
],
runtime: [
'.claude-flow',
Expand Down Expand Up @@ -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<void> {
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
*/
Expand All @@ -767,6 +819,11 @@ async function writeHelpers(
): Promise<void> {
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);

Expand Down
Loading
Loading