create-merlin-brain 3.11.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/install.cjs +156 -32
- package/bin/runtime-adapters.cjs +396 -0
- package/dist/server/api/types.d.ts +7 -0
- package/dist/server/api/types.d.ts.map +1 -1
- package/dist/server/cost/tracker.d.ts +38 -2
- package/dist/server/cost/tracker.d.ts.map +1 -1
- package/dist/server/cost/tracker.js +87 -15
- package/dist/server/cost/tracker.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +74 -30
- package/dist/server/server.js.map +1 -1
- package/dist/server/tools/__tests__/augmentation.test.d.ts +8 -0
- package/dist/server/tools/__tests__/augmentation.test.d.ts.map +1 -0
- package/dist/server/tools/__tests__/augmentation.test.js +76 -0
- package/dist/server/tools/__tests__/augmentation.test.js.map +1 -0
- package/dist/server/tools/__tests__/route-helpers.test.d.ts +5 -0
- package/dist/server/tools/__tests__/route-helpers.test.d.ts.map +1 -0
- package/dist/server/tools/__tests__/route-helpers.test.js +49 -0
- package/dist/server/tools/__tests__/route-helpers.test.js.map +1 -0
- package/dist/server/tools/adaptive.js +1 -1
- package/dist/server/tools/adaptive.js.map +1 -1
- package/dist/server/tools/agent-spawn.d.ts +25 -0
- package/dist/server/tools/agent-spawn.d.ts.map +1 -0
- package/dist/server/tools/agent-spawn.js +95 -0
- package/dist/server/tools/agent-spawn.js.map +1 -0
- package/dist/server/tools/agents-index.js +3 -3
- package/dist/server/tools/agents-index.js.map +1 -1
- package/dist/server/tools/agents.js +5 -5
- package/dist/server/tools/agents.js.map +1 -1
- package/dist/server/tools/augmentation.d.ts +45 -0
- package/dist/server/tools/augmentation.d.ts.map +1 -0
- package/dist/server/tools/augmentation.js +167 -0
- package/dist/server/tools/augmentation.js.map +1 -0
- package/dist/server/tools/behaviors.js +4 -4
- package/dist/server/tools/behaviors.js.map +1 -1
- package/dist/server/tools/context.js +7 -7
- package/dist/server/tools/context.js.map +1 -1
- package/dist/server/tools/cost.d.ts +3 -1
- package/dist/server/tools/cost.d.ts.map +1 -1
- package/dist/server/tools/cost.js +66 -13
- package/dist/server/tools/cost.js.map +1 -1
- package/dist/server/tools/discoveries.js +6 -6
- package/dist/server/tools/discoveries.js.map +1 -1
- package/dist/server/tools/index.d.ts +4 -0
- package/dist/server/tools/index.d.ts.map +1 -1
- package/dist/server/tools/index.js +4 -0
- package/dist/server/tools/index.js.map +1 -1
- package/dist/server/tools/learning.d.ts +12 -0
- package/dist/server/tools/learning.d.ts.map +1 -0
- package/dist/server/tools/learning.js +269 -0
- package/dist/server/tools/learning.js.map +1 -0
- package/dist/server/tools/project.js +7 -7
- package/dist/server/tools/project.js.map +1 -1
- package/dist/server/tools/promote.d.ts +11 -0
- package/dist/server/tools/promote.d.ts.map +1 -0
- package/dist/server/tools/promote.js +315 -0
- package/dist/server/tools/promote.js.map +1 -0
- package/dist/server/tools/route-helpers.d.ts +45 -0
- package/dist/server/tools/route-helpers.d.ts.map +1 -0
- package/dist/server/tools/route-helpers.js +93 -0
- package/dist/server/tools/route-helpers.js.map +1 -0
- package/dist/server/tools/route.d.ts +4 -3
- package/dist/server/tools/route.d.ts.map +1 -1
- package/dist/server/tools/route.js +80 -284
- package/dist/server/tools/route.js.map +1 -1
- package/dist/server/tools/session-restore.d.ts +18 -0
- package/dist/server/tools/session-restore.d.ts.map +1 -0
- package/dist/server/tools/session-restore.js +154 -0
- package/dist/server/tools/session-restore.js.map +1 -0
- package/dist/server/tools/session-search.d.ts +16 -0
- package/dist/server/tools/session-search.d.ts.map +1 -0
- package/dist/server/tools/session-search.js +240 -0
- package/dist/server/tools/session-search.js.map +1 -0
- package/dist/server/tools/sights-index.js +2 -2
- package/dist/server/tools/sights-index.js.map +1 -1
- package/dist/server/tools/smart-route.d.ts.map +1 -1
- package/dist/server/tools/smart-route.js +4 -5
- package/dist/server/tools/smart-route.js.map +1 -1
- package/dist/server/tools/verification.js +1 -1
- package/dist/server/tools/verification.js.map +1 -1
- package/files/agents/code-organization-supervisor.md +1 -0
- package/files/agents/context-guardian.md +1 -0
- package/files/agents/docs-keeper.md +1 -0
- package/files/agents/dry-refactor.md +1 -0
- package/files/agents/elite-code-refactorer.md +1 -0
- package/files/agents/hardening-guard.md +1 -0
- package/files/agents/implementation-dev.md +1 -0
- package/files/agents/merlin-access-control-reviewer.md +248 -0
- package/files/agents/merlin-codebase-mapper.md +1 -1
- package/files/agents/merlin-dependency-auditor.md +216 -0
- package/files/agents/merlin-executor.md +1 -0
- package/files/agents/merlin-input-validator.md +247 -0
- package/files/agents/merlin-reviewer.md +1 -0
- package/files/agents/merlin-sast-reviewer.md +182 -0
- package/files/agents/merlin-secret-scanner.md +203 -0
- package/files/agents/tests-qa.md +1 -0
- package/files/commands/merlin/execute-phase.md +94 -197
- package/files/commands/merlin/execute-plan.md +116 -180
- package/files/commands/merlin/health.md +385 -0
- package/files/commands/merlin/loop-recipes.md +93 -36
- package/files/commands/merlin/optimize-prompts.md +158 -0
- package/files/commands/merlin/profiles.md +215 -0
- package/files/commands/merlin/promote.md +176 -0
- package/files/commands/merlin/quick.md +229 -0
- package/files/commands/merlin/resume-work.md +27 -1
- package/files/commands/merlin/route.md +43 -1
- package/files/commands/merlin/sandbox.md +359 -0
- package/files/commands/merlin/usage.md +55 -0
- package/files/docker/Dockerfile.merlin +20 -0
- package/files/docker/docker-compose.merlin.yml +23 -0
- package/files/hook-templates/auto-commit.sh +64 -0
- package/files/hook-templates/auto-format.sh +95 -0
- package/files/hook-templates/auto-test.sh +117 -0
- package/files/hook-templates/branch-protection.sh +72 -0
- package/files/hook-templates/changelog-reminder.sh +76 -0
- package/files/hook-templates/complexity-check.sh +112 -0
- package/files/hook-templates/import-audit.sh +83 -0
- package/files/hook-templates/license-header.sh +84 -0
- package/files/hook-templates/pr-description.sh +100 -0
- package/files/hook-templates/todo-tracker.sh +80 -0
- package/files/hooks/check-file-size.sh +17 -4
- package/files/hooks/config-change.sh +44 -16
- package/files/hooks/instructions-loaded.sh +22 -5
- package/files/hooks/notify-desktop.sh +157 -0
- package/files/hooks/notify-webhook.sh +141 -0
- package/files/hooks/pre-edit-sights-check.sh +76 -9
- package/files/hooks/security-scanner.sh +153 -0
- package/files/hooks/session-end-memory-sync.sh +97 -0
- package/files/hooks/session-end.sh +274 -1
- package/files/hooks/session-start.sh +19 -6
- package/files/hooks/smart-approve.sh +270 -0
- package/files/hooks/teammate-idle-verify.sh +87 -12
- package/files/hooks/worktree-create.sh +20 -3
- package/files/hooks/worktree-remove.sh +21 -3
- package/files/merlin/references/plan-format.md +37 -9
- package/files/merlin/sandbox.json +9 -0
- package/files/merlin/security.json +11 -0
- package/files/merlin/templates/ci/docs-update.yml +81 -0
- package/files/merlin/templates/ci/pr-review.yml +50 -0
- package/files/merlin/templates/ci/security-audit.yml +74 -0
- package/files/merlin/templates/config.json +9 -1
- package/files/rules/api-rules.md +30 -0
- package/files/rules/frontend-rules.md +25 -0
- package/files/rules/hooks-rules.md +36 -0
- package/files/rules/mcp-rules.md +30 -0
- package/files/rules/worker-rules.md +29 -0
- package/package.json +5 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Hook Template: todo-tracker.sh
|
|
4
|
+
# Event: PostToolUse (Write, Edit)
|
|
5
|
+
#
|
|
6
|
+
# Scans modified files for new TODO, FIXME, HACK, XXX, and TEMP comments.
|
|
7
|
+
# Reports them so they don't silently accumulate without notice.
|
|
8
|
+
#
|
|
9
|
+
# HOW TO INSTALL:
|
|
10
|
+
# Copy this file to ~/.claude/merlin/hooks/todo-tracker.sh
|
|
11
|
+
# Then add to your .claude/settings.local.json:
|
|
12
|
+
#
|
|
13
|
+
# {
|
|
14
|
+
# "hooks": {
|
|
15
|
+
# "PostToolUse": [
|
|
16
|
+
# {
|
|
17
|
+
# "matcher": "Write|Edit",
|
|
18
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/merlin/hooks/todo-tracker.sh" }]
|
|
19
|
+
# }
|
|
20
|
+
# ]
|
|
21
|
+
# }
|
|
22
|
+
# }
|
|
23
|
+
#
|
|
24
|
+
# BEHAVIOR: Advisory only. Lists new TODO-style comments found in the changed file.
|
|
25
|
+
# Exits 0 (no blocking) — just surfaces the information.
|
|
26
|
+
#
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
trap 'echo "{}"; exit 0' ERR
|
|
29
|
+
|
|
30
|
+
TODO_PATTERN='TODO|FIXME|HACK|XXX|TEMP|NOCOMMIT|REMOVEME'
|
|
31
|
+
|
|
32
|
+
# Read tool input from stdin
|
|
33
|
+
input=""
|
|
34
|
+
if [ ! -t 0 ]; then
|
|
35
|
+
input=$(cat 2>/dev/null || true)
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
[ -z "$input" ] && { echo '{}'; exit 0; }
|
|
39
|
+
|
|
40
|
+
# Extract file path
|
|
41
|
+
file_path=""
|
|
42
|
+
if command -v jq >/dev/null 2>&1; then
|
|
43
|
+
file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null || true)
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
[ -z "$file_path" ] || [ ! -f "$file_path" ] && { echo '{}'; exit 0; }
|
|
47
|
+
|
|
48
|
+
# Only scan source code files
|
|
49
|
+
case "$file_path" in
|
|
50
|
+
*.js|*.jsx|*.ts|*.tsx|*.mjs|*.py|*.go|*.java|*.rb|*.rs|*.swift|*.c|*.cpp|*.h|*.sh|*.md)
|
|
51
|
+
;;
|
|
52
|
+
*)
|
|
53
|
+
echo '{}'
|
|
54
|
+
exit 0
|
|
55
|
+
;;
|
|
56
|
+
esac
|
|
57
|
+
|
|
58
|
+
# Skip node_modules and generated files
|
|
59
|
+
case "$file_path" in
|
|
60
|
+
*node_modules*|*dist/*|*build/*|*vendor/*)
|
|
61
|
+
echo '{}'
|
|
62
|
+
exit 0
|
|
63
|
+
;;
|
|
64
|
+
esac
|
|
65
|
+
|
|
66
|
+
# Find TODO-style comments with line numbers
|
|
67
|
+
todos=$(grep -nE "$TODO_PATTERN" "$file_path" 2>/dev/null | head -20 || true)
|
|
68
|
+
|
|
69
|
+
if [ -n "$todos" ]; then
|
|
70
|
+
count=$(echo "$todos" | wc -l | tr -d ' ')
|
|
71
|
+
echo "TODO TRACKER: ${count} annotation(s) found in ${file_path}:" >&2
|
|
72
|
+
echo "$todos" | while IFS= read -r line; do
|
|
73
|
+
echo " $line" >&2
|
|
74
|
+
done
|
|
75
|
+
echo " Track these in your issue tracker to avoid tech debt accumulation." >&2
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Always exit 0 — informational only, never blocking
|
|
79
|
+
echo '{}'
|
|
80
|
+
exit 0
|
|
@@ -4,16 +4,29 @@
|
|
|
4
4
|
# Checks if the modified file exceeds the 400-line convention.
|
|
5
5
|
# Exits with code 2 to inject feedback when file is too large.
|
|
6
6
|
#
|
|
7
|
-
# Agent-type awareness:
|
|
8
|
-
#
|
|
7
|
+
# Agent-type awareness: only enforce for implementation agents.
|
|
8
|
+
# Non-code agents (docs, review, verify, etc.) are fully exempt.
|
|
9
9
|
#
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
trap 'echo "{}"; exit 0' ERR
|
|
12
12
|
|
|
13
|
-
#
|
|
13
|
+
# Read CLAUDE_AGENT_TYPE from env — support both var names
|
|
14
14
|
AGENT_TYPE="${CLAUDE_AGENT_TYPE:-${CLAUDE_CODE_AGENT_TYPE:-main}}"
|
|
15
|
+
|
|
16
|
+
# Only enforce for implementation agents
|
|
17
|
+
# Skip for all doc/review/verification/analysis agents
|
|
15
18
|
case "$AGENT_TYPE" in
|
|
16
|
-
|
|
19
|
+
implementation-dev|merlin-executor)
|
|
20
|
+
# Enforcement is active for these agents — continue
|
|
21
|
+
;;
|
|
22
|
+
docs-keeper|merlin-reviewer|merlin-verifier|merlin-milestone-auditor|\
|
|
23
|
+
merlin-integration-checker|merlin-work-verifier|code-organization-supervisor|\
|
|
24
|
+
context-guardian|dry-refactor|tests-qa|merlin-codebase-mapper)
|
|
25
|
+
echo '{}'
|
|
26
|
+
exit 0
|
|
27
|
+
;;
|
|
28
|
+
*)
|
|
29
|
+
# For all other agents (including 'main'), skip enforcement
|
|
17
30
|
echo '{}'
|
|
18
31
|
exit 0
|
|
19
32
|
;;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Merlin Hook: ConfigChange
|
|
4
4
|
# Purpose: Validate Merlin MCP key still works after config change.
|
|
5
|
-
#
|
|
5
|
+
# Logs config change events to Sights for audit trail.
|
|
6
6
|
#
|
|
7
7
|
# Always exits 0 — never blocks Claude Code.
|
|
8
8
|
#
|
|
@@ -13,31 +13,59 @@ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
13
13
|
|
|
14
14
|
# Source shared libs if available
|
|
15
15
|
[ -f "${HOOK_DIR}/lib/analytics.sh" ] && . "${HOOK_DIR}/lib/analytics.sh"
|
|
16
|
+
[ -f "${HOOK_DIR}/lib/sights-check.sh" ] && . "${HOOK_DIR}/lib/sights-check.sh"
|
|
16
17
|
|
|
17
|
-
MERLIN_API_KEY="${MERLIN_API_KEY:-}"
|
|
18
18
|
AGENT_ID="${CLAUDE_AGENT_ID:-unknown}"
|
|
19
19
|
AGENT_TYPE="${CLAUDE_AGENT_TYPE:-main}"
|
|
20
|
+
CLAUDE_DIR="${HOME}/.claude"
|
|
21
|
+
|
|
22
|
+
# Resolve API key: env var takes priority, fall back to config.json
|
|
23
|
+
MERLIN_API_KEY="${MERLIN_API_KEY:-}"
|
|
24
|
+
if [ -z "$MERLIN_API_KEY" ] && declare -f get_merlin_api_key >/dev/null 2>&1; then
|
|
25
|
+
MERLIN_API_KEY="$(get_merlin_api_key)"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Detect which MCP servers are configured for audit trail
|
|
29
|
+
MCP_SERVERS=""
|
|
30
|
+
if [ -f "${CLAUDE_DIR}/config.json" ] && command -v jq >/dev/null 2>&1; then
|
|
31
|
+
MCP_SERVERS=$(jq -r '(.mcpServers // {}) | keys | join(",")' \
|
|
32
|
+
"${CLAUDE_DIR}/config.json" 2>/dev/null || echo "")
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
HAS_KEY="$([ -n "$MERLIN_API_KEY" ] && echo true || echo false)"
|
|
36
|
+
KEY_VALID="unknown"
|
|
37
|
+
|
|
38
|
+
# Validate key format if present (valid prefixes: mrln_ or ccw_)
|
|
39
|
+
if [ -n "$MERLIN_API_KEY" ]; then
|
|
40
|
+
case "$MERLIN_API_KEY" in
|
|
41
|
+
mrln_*|ccw_*)
|
|
42
|
+
KEY_VALID="true"
|
|
43
|
+
;;
|
|
44
|
+
*)
|
|
45
|
+
KEY_VALID="false"
|
|
46
|
+
echo "Merlin: API key has unexpected format after config change" >&2
|
|
47
|
+
;;
|
|
48
|
+
esac
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Write audit trail entry to local log
|
|
52
|
+
AUDIT_DIR="${HOME}/.claude/merlin/audit"
|
|
53
|
+
mkdir -p "$AUDIT_DIR" 2>/dev/null || true
|
|
54
|
+
printf '{"ts":"%s","agent_id":"%s","agent_type":"%s","has_key":%s,"key_valid":"%s","mcp_servers":"%s"}\n' \
|
|
55
|
+
"$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
|
56
|
+
"$AGENT_ID" "$AGENT_TYPE" "$HAS_KEY" "$KEY_VALID" "$MCP_SERVERS" \
|
|
57
|
+
>> "${AUDIT_DIR}/config-changes.log" 2>/dev/null || true
|
|
20
58
|
|
|
21
59
|
# Log config change event if analytics available
|
|
22
60
|
if declare -f log_event >/dev/null 2>&1; then
|
|
23
|
-
log_event "config_change" "$(printf
|
|
61
|
+
log_event "config_change" "$(printf \
|
|
62
|
+
'{"agent_id":"%s","agent_type":"%s","has_key":%s,"key_valid":"%s","mcp_servers":"%s"}' \
|
|
63
|
+
"$AGENT_ID" "$AGENT_TYPE" "$HAS_KEY" "$KEY_VALID" "$MCP_SERVERS")"
|
|
24
64
|
fi
|
|
25
65
|
|
|
26
66
|
if [ -z "$MERLIN_API_KEY" ]; then
|
|
27
|
-
echo "Merlin: No API key configured
|
|
28
|
-
echo '{}'
|
|
29
|
-
exit 0
|
|
67
|
+
echo "Merlin: No API key configured — Sights features disabled" >&2
|
|
30
68
|
fi
|
|
31
69
|
|
|
32
|
-
# Quick validation — check key format (valid prefixes: mrln_ or ccw_)
|
|
33
|
-
case "$MERLIN_API_KEY" in
|
|
34
|
-
mrln_*|ccw_*)
|
|
35
|
-
# Valid prefix — no-op
|
|
36
|
-
;;
|
|
37
|
-
*)
|
|
38
|
-
echo "Merlin: API key has unexpected format after config change" >&2
|
|
39
|
-
;;
|
|
40
|
-
esac
|
|
41
|
-
|
|
42
70
|
echo '{}'
|
|
43
71
|
exit 0
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
# Purpose: Pre-warm Merlin Sights connection when CLAUDE.md loads.
|
|
5
5
|
# Fires BEFORE session-start, giving us a head start on context loading.
|
|
6
6
|
#
|
|
7
|
+
# Cold-start optimization: touching the sights-check timestamp file signals
|
|
8
|
+
# that Merlin is active so the first pre-edit check doesn't fire a warning.
|
|
9
|
+
#
|
|
7
10
|
# Always exits 0 — never blocks Claude Code startup.
|
|
8
11
|
#
|
|
9
12
|
set -euo pipefail
|
|
@@ -17,18 +20,32 @@ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
17
20
|
# shellcheck source=lib/sights-check.sh
|
|
18
21
|
[ -f "${HOOK_DIR}/lib/sights-check.sh" ] && . "${HOOK_DIR}/lib/sights-check.sh"
|
|
19
22
|
|
|
20
|
-
# Log agent context if available
|
|
21
23
|
AGENT_ID="${CLAUDE_AGENT_ID:-unknown}"
|
|
22
24
|
AGENT_TYPE="${CLAUDE_AGENT_TYPE:-main}"
|
|
25
|
+
MERLIN_DIR="${HOME}/.claude/merlin"
|
|
26
|
+
|
|
27
|
+
# Cold-start pre-warm: record that Sights is active so the pre-edit hook
|
|
28
|
+
# doesn't fire a "Sights not consulted" warning immediately after boot.
|
|
29
|
+
# Only touch if the file is stale (>300s old) or missing — avoids
|
|
30
|
+
# re-stamping on every CLAUDE.md reload within the same session.
|
|
31
|
+
if declare -f record_sights_call >/dev/null 2>&1; then
|
|
32
|
+
if ! sights_was_checked_recently 300 2>/dev/null; then
|
|
33
|
+
record_sights_call 2>/dev/null || true
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Write a lightweight cold-start marker so session-start can detect
|
|
38
|
+
# that instructions were already loaded (avoids double context fetch).
|
|
39
|
+
mkdir -p "${MERLIN_DIR}" 2>/dev/null || true
|
|
40
|
+
printf '%s' "$(date +%s)" > "${MERLIN_DIR}/.instructions-loaded-ts" 2>/dev/null || true
|
|
23
41
|
|
|
24
42
|
# Log the instructions-loaded event if analytics is available
|
|
25
43
|
if declare -f log_event >/dev/null 2>&1; then
|
|
26
|
-
log_event "instructions_loaded" "$(printf '{"agent_id":"%s","agent_type":"%s","cwd":"%s"}'
|
|
44
|
+
log_event "instructions_loaded" "$(printf '{"agent_id":"%s","agent_type":"%s","cwd":"%s"}' \
|
|
45
|
+
"$AGENT_ID" "$AGENT_TYPE" "${PWD:-}")"
|
|
27
46
|
fi
|
|
28
47
|
|
|
29
|
-
|
|
30
|
-
# The MCP server will handle actual Sights connection
|
|
31
|
-
echo "Merlin instructions loaded — Sights pre-warming (agent: ${AGENT_TYPE})" >&2
|
|
48
|
+
echo "Merlin instructions loaded — Sights pre-warmed (agent: ${AGENT_TYPE})" >&2
|
|
32
49
|
|
|
33
50
|
echo '{}'
|
|
34
51
|
exit 0
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Merlin Hook: notify-desktop.sh
|
|
4
|
+
# Events: Notification, Stop
|
|
5
|
+
#
|
|
6
|
+
# Sends a desktop notification when Claude finishes work or needs input.
|
|
7
|
+
# Reads notification config from .merlin/config.json in the project root.
|
|
8
|
+
# Always exits 0 — notifications must never block the main session.
|
|
9
|
+
#
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
trap 'echo "{}"; exit 0' ERR
|
|
12
|
+
|
|
13
|
+
# ── Config resolution ─────────────────────────────────────────────
|
|
14
|
+
# Resolve project root: prefer CLAUDE_WORKTREE_PATH, fall back to PWD
|
|
15
|
+
PROJECT_ROOT="${CLAUDE_WORKTREE_PATH:-${PWD:-$(pwd)}}"
|
|
16
|
+
CONFIG_FILE="${PROJECT_ROOT}/.merlin/config.json"
|
|
17
|
+
|
|
18
|
+
# Read a value from .merlin/config.json using jq or python3 as fallback
|
|
19
|
+
_read_config() {
|
|
20
|
+
local key="${1}" default="${2:-}"
|
|
21
|
+
if [ ! -f "${CONFIG_FILE}" ]; then
|
|
22
|
+
echo "${default}"
|
|
23
|
+
return 0
|
|
24
|
+
fi
|
|
25
|
+
local val
|
|
26
|
+
if command -v jq >/dev/null 2>&1; then
|
|
27
|
+
val=$(jq -r "${key} // \"${default}\"" "${CONFIG_FILE}" 2>/dev/null) || val="${default}"
|
|
28
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
29
|
+
val=$(python3 -c "
|
|
30
|
+
import json, sys
|
|
31
|
+
try:
|
|
32
|
+
d = json.load(open('${CONFIG_FILE}'))
|
|
33
|
+
keys = '${key}'.lstrip('.').split('.')
|
|
34
|
+
v = d
|
|
35
|
+
for k in keys:
|
|
36
|
+
v = v.get(k, None)
|
|
37
|
+
if v is None:
|
|
38
|
+
break
|
|
39
|
+
print(v if v is not None else '${default}')
|
|
40
|
+
except:
|
|
41
|
+
print('${default}')
|
|
42
|
+
" 2>/dev/null) || val="${default}"
|
|
43
|
+
else
|
|
44
|
+
val="${default}"
|
|
45
|
+
fi
|
|
46
|
+
echo "${val}"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Check if desktop notifications are enabled (default: true)
|
|
50
|
+
DESKTOP_ENABLED=$(_read_config '.notifications.desktop' 'true')
|
|
51
|
+
if [ "${DESKTOP_ENABLED}" = "false" ]; then
|
|
52
|
+
echo '{}'
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Check notify_on list to see if this event type is included
|
|
57
|
+
HOOK_EVENT="${CLAUDE_HOOK_EVENT:-Stop}"
|
|
58
|
+
NOTIFY_ON=$(_read_config '.notifications.notify_on' '["stop","needs_input","error"]')
|
|
59
|
+
|
|
60
|
+
# Map hook event names to notify_on values
|
|
61
|
+
case "${HOOK_EVENT}" in
|
|
62
|
+
Stop) EVENT_KEY="stop" ;;
|
|
63
|
+
Notification) EVENT_KEY="needs_input" ;;
|
|
64
|
+
*) EVENT_KEY="stop" ;;
|
|
65
|
+
esac
|
|
66
|
+
|
|
67
|
+
# Check if this event is in the notify_on list
|
|
68
|
+
EVENT_ENABLED=false
|
|
69
|
+
if echo "${NOTIFY_ON}" | grep -q "\"${EVENT_KEY}\"" 2>/dev/null; then
|
|
70
|
+
EVENT_ENABLED=true
|
|
71
|
+
fi
|
|
72
|
+
# Default to true if we couldn't parse (degraded gracefully)
|
|
73
|
+
if [ "${NOTIFY_ON}" = '["stop","needs_input","error"]' ]; then
|
|
74
|
+
EVENT_ENABLED=true
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
if [ "${EVENT_ENABLED}" = "false" ]; then
|
|
78
|
+
echo '{}'
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# ── Build notification message ─────────────────────────────────────
|
|
83
|
+
# Read stdin for hook context (non-blocking, best-effort)
|
|
84
|
+
HOOK_INPUT=""
|
|
85
|
+
if [ -t 0 ]; then
|
|
86
|
+
# stdin is a terminal, no piped input
|
|
87
|
+
HOOK_INPUT=""
|
|
88
|
+
else
|
|
89
|
+
HOOK_INPUT=$(cat 2>/dev/null || true)
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Determine message based on event type
|
|
93
|
+
case "${HOOK_EVENT}" in
|
|
94
|
+
Stop)
|
|
95
|
+
# Extract agent type for context if available
|
|
96
|
+
AGENT_TYPE="${CLAUDE_AGENT_TYPE:-main}"
|
|
97
|
+
if [ "${AGENT_TYPE}" != "main" ] && [ "${AGENT_TYPE}" != "unknown" ]; then
|
|
98
|
+
MESSAGE="Task complete (${AGENT_TYPE} agent)"
|
|
99
|
+
else
|
|
100
|
+
MESSAGE="Task complete — Claude has finished working"
|
|
101
|
+
fi
|
|
102
|
+
;;
|
|
103
|
+
Notification)
|
|
104
|
+
MESSAGE="Claude needs your input"
|
|
105
|
+
;;
|
|
106
|
+
*)
|
|
107
|
+
MESSAGE="Merlin: Task complete"
|
|
108
|
+
;;
|
|
109
|
+
esac
|
|
110
|
+
|
|
111
|
+
TITLE="Merlin"
|
|
112
|
+
SOUND_ENABLED=$(_read_config '.notifications.sound' 'false')
|
|
113
|
+
|
|
114
|
+
# ── Detect OS and send notification ───────────────────────────────
|
|
115
|
+
OS_TYPE="$(uname -s 2>/dev/null || echo 'unknown')"
|
|
116
|
+
|
|
117
|
+
case "${OS_TYPE}" in
|
|
118
|
+
Darwin)
|
|
119
|
+
# macOS — use osascript
|
|
120
|
+
osascript -e "display notification \"${MESSAGE}\" with title \"${TITLE}\" sound name \"Glass\"" \
|
|
121
|
+
>/dev/null 2>&1 &
|
|
122
|
+
|
|
123
|
+
# Optional extra sound (beyond what osascript plays)
|
|
124
|
+
if [ "${SOUND_ENABLED}" = "true" ]; then
|
|
125
|
+
SOUND_FILE="/System/Library/Sounds/Glass.aiff"
|
|
126
|
+
if [ -f "${SOUND_FILE}" ]; then
|
|
127
|
+
afplay "${SOUND_FILE}" >/dev/null 2>&1 &
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
;;
|
|
131
|
+
|
|
132
|
+
Linux)
|
|
133
|
+
# Linux — use notify-send if available
|
|
134
|
+
if command -v notify-send >/dev/null 2>&1; then
|
|
135
|
+
notify-send "${TITLE}" "${MESSAGE}" --icon=dialog-information \
|
|
136
|
+
>/dev/null 2>&1 &
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# Optional sound on Linux
|
|
140
|
+
if [ "${SOUND_ENABLED}" = "true" ]; then
|
|
141
|
+
SOUND_FILE="/usr/share/sounds/freedesktop/stereo/complete.oga"
|
|
142
|
+
if [ -f "${SOUND_FILE}" ] && command -v paplay >/dev/null 2>&1; then
|
|
143
|
+
paplay "${SOUND_FILE}" >/dev/null 2>&1 &
|
|
144
|
+
fi
|
|
145
|
+
fi
|
|
146
|
+
;;
|
|
147
|
+
|
|
148
|
+
*)
|
|
149
|
+
# Unknown OS — exit silently
|
|
150
|
+
echo '{}'
|
|
151
|
+
exit 0
|
|
152
|
+
;;
|
|
153
|
+
esac
|
|
154
|
+
|
|
155
|
+
# Claude Code command hooks must output valid JSON to stdout
|
|
156
|
+
echo '{}'
|
|
157
|
+
exit 0
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Merlin Hook: notify-webhook.sh
|
|
4
|
+
# Event: Stop
|
|
5
|
+
#
|
|
6
|
+
# Fires Slack and/or Discord webhooks when Claude finishes a task.
|
|
7
|
+
# Reads webhook URLs from .merlin/config.json in the project root.
|
|
8
|
+
# All curl calls are fire-and-forget (background, 5s timeout).
|
|
9
|
+
# Always exits 0 — webhooks must never block the main session.
|
|
10
|
+
#
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
trap 'echo "{}"; exit 0' ERR
|
|
13
|
+
|
|
14
|
+
# ── Require curl ──────────────────────────────────────────────────
|
|
15
|
+
if ! command -v curl >/dev/null 2>&1; then
|
|
16
|
+
echo '{}'
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# ── Config resolution ─────────────────────────────────────────────
|
|
21
|
+
PROJECT_ROOT="${CLAUDE_WORKTREE_PATH:-${PWD:-$(pwd)}}"
|
|
22
|
+
CONFIG_FILE="${PROJECT_ROOT}/.merlin/config.json"
|
|
23
|
+
|
|
24
|
+
_read_config() {
|
|
25
|
+
local key="${1}" default="${2:-}"
|
|
26
|
+
if [ ! -f "${CONFIG_FILE}" ]; then
|
|
27
|
+
echo "${default}"
|
|
28
|
+
return 0
|
|
29
|
+
fi
|
|
30
|
+
local val
|
|
31
|
+
if command -v jq >/dev/null 2>&1; then
|
|
32
|
+
val=$(jq -r "${key} // \"${default}\"" "${CONFIG_FILE}" 2>/dev/null) || val="${default}"
|
|
33
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
34
|
+
val=$(python3 -c "
|
|
35
|
+
import json, sys
|
|
36
|
+
try:
|
|
37
|
+
d = json.load(open('${CONFIG_FILE}'))
|
|
38
|
+
keys = '${key}'.lstrip('.').split('.')
|
|
39
|
+
v = d
|
|
40
|
+
for k in keys:
|
|
41
|
+
v = v.get(k, None)
|
|
42
|
+
if v is None:
|
|
43
|
+
break
|
|
44
|
+
print(v if v is not None else '${default}')
|
|
45
|
+
except:
|
|
46
|
+
print('${default}')
|
|
47
|
+
" 2>/dev/null) || val="${default}"
|
|
48
|
+
else
|
|
49
|
+
val="${default}"
|
|
50
|
+
fi
|
|
51
|
+
echo "${val}"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
SLACK_WEBHOOK=$(_read_config '.notifications.slack_webhook' '')
|
|
55
|
+
DISCORD_WEBHOOK=$(_read_config '.notifications.discord_webhook' '')
|
|
56
|
+
|
|
57
|
+
# Exit early if no webhooks are configured
|
|
58
|
+
if [ -z "${SLACK_WEBHOOK}" ] && [ -z "${DISCORD_WEBHOOK}" ]; then
|
|
59
|
+
echo '{}'
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# ── Build context ─────────────────────────────────────────────────
|
|
64
|
+
AGENT_NAME="${CLAUDE_AGENT_TYPE:-main}"
|
|
65
|
+
if [ "${AGENT_NAME}" = "unknown" ] || [ -z "${AGENT_NAME}" ]; then
|
|
66
|
+
AGENT_NAME="main"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Calculate session duration from session analytics file if available
|
|
70
|
+
SESSION_ID="${MERLIN_SESSION_ID:-}"
|
|
71
|
+
DURATION_STR="unknown"
|
|
72
|
+
if [ -n "${SESSION_ID}" ]; then
|
|
73
|
+
SESSION_FILE="${HOME}/.claude/merlin/analytics/session-${SESSION_ID}.json"
|
|
74
|
+
if [ -f "${SESSION_FILE}" ] && command -v jq >/dev/null 2>&1; then
|
|
75
|
+
START_TS=$(jq -r '.startTime // ""' "${SESSION_FILE}" 2>/dev/null || echo "")
|
|
76
|
+
if [ -n "${START_TS}" ]; then
|
|
77
|
+
START_EPOCH=$(date -d "${START_TS}" +%s 2>/dev/null \
|
|
78
|
+
|| python3 -c "import datetime; print(int(datetime.datetime.fromisoformat('${START_TS}'.replace('Z','+00:00')).timestamp()))" 2>/dev/null \
|
|
79
|
+
|| echo "")
|
|
80
|
+
if [ -n "${START_EPOCH}" ]; then
|
|
81
|
+
NOW_EPOCH=$(date +%s)
|
|
82
|
+
ELAPSED=$(( NOW_EPOCH - START_EPOCH ))
|
|
83
|
+
MINUTES=$(( ELAPSED / 60 ))
|
|
84
|
+
SECONDS=$(( ELAPSED % 60 ))
|
|
85
|
+
DURATION_STR="${MINUTES}m ${SECONDS}s"
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
STATUS_TEXT="Success"
|
|
92
|
+
STATUS_ICON="OK"
|
|
93
|
+
|
|
94
|
+
# ── Slack webhook ─────────────────────────────────────────────────
|
|
95
|
+
if [ -n "${SLACK_WEBHOOK}" ]; then
|
|
96
|
+
SLACK_BODY=$(cat <<SLACK_JSON
|
|
97
|
+
{
|
|
98
|
+
"text": "Merlin: Task completed",
|
|
99
|
+
"blocks": [
|
|
100
|
+
{
|
|
101
|
+
"type": "section",
|
|
102
|
+
"text": {
|
|
103
|
+
"type": "mrkdwn",
|
|
104
|
+
"text": "*Merlin Task Complete*\n\nAgent: ${AGENT_NAME}\nDuration: ${DURATION_STR}\nStatus: ${STATUS_ICON} ${STATUS_TEXT}"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
SLACK_JSON
|
|
110
|
+
)
|
|
111
|
+
curl \
|
|
112
|
+
--max-time 5 \
|
|
113
|
+
--silent \
|
|
114
|
+
--output /dev/null \
|
|
115
|
+
--request POST \
|
|
116
|
+
--header "Content-Type: application/json" \
|
|
117
|
+
--data "${SLACK_BODY}" \
|
|
118
|
+
"${SLACK_WEBHOOK}" &
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# ── Discord webhook ───────────────────────────────────────────────
|
|
122
|
+
if [ -n "${DISCORD_WEBHOOK}" ]; then
|
|
123
|
+
DISCORD_BODY=$(cat <<DISCORD_JSON
|
|
124
|
+
{
|
|
125
|
+
"content": "**Merlin Task Complete**\n\nAgent: ${AGENT_NAME}\nDuration: ${DURATION_STR}\nStatus: ${STATUS_ICON} ${STATUS_TEXT}"
|
|
126
|
+
}
|
|
127
|
+
DISCORD_JSON
|
|
128
|
+
)
|
|
129
|
+
curl \
|
|
130
|
+
--max-time 5 \
|
|
131
|
+
--silent \
|
|
132
|
+
--output /dev/null \
|
|
133
|
+
--request POST \
|
|
134
|
+
--header "Content-Type: application/json" \
|
|
135
|
+
--data "${DISCORD_BODY}" \
|
|
136
|
+
"${DISCORD_WEBHOOK}" &
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# Claude Code command hooks must output valid JSON to stdout
|
|
140
|
+
echo '{}'
|
|
141
|
+
exit 0
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
3
|
# Merlin Hook: PreToolUse (Edit/Write)
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Two responsibilities:
|
|
5
|
+
# 1. Secret detection — blocks writes that contain leaked credentials.
|
|
6
|
+
# Exit code 2 with a clear message. This is the only hard block.
|
|
7
|
+
# 2. Sights consultation check — advisory warning if Sights was not
|
|
8
|
+
# consulted recently. Always exits 0 (never blocks on this).
|
|
6
9
|
#
|
|
7
10
|
set -euo pipefail
|
|
8
11
|
trap 'echo "{}"; exit 0' ERR
|
|
@@ -11,9 +14,9 @@ HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
11
14
|
|
|
12
15
|
# Source shared libraries
|
|
13
16
|
# shellcheck source=lib/analytics.sh
|
|
14
|
-
. "${HOOKS_DIR}/lib/analytics.sh"
|
|
17
|
+
[ -f "${HOOKS_DIR}/lib/analytics.sh" ] && . "${HOOKS_DIR}/lib/analytics.sh"
|
|
15
18
|
# shellcheck source=lib/sights-check.sh
|
|
16
|
-
. "${HOOKS_DIR}/lib/sights-check.sh"
|
|
19
|
+
[ -f "${HOOKS_DIR}/lib/sights-check.sh" ] && . "${HOOKS_DIR}/lib/sights-check.sh"
|
|
17
20
|
|
|
18
21
|
# Read tool input from stdin (Claude Code pipes JSON)
|
|
19
22
|
input=""
|
|
@@ -21,19 +24,83 @@ if [ ! -t 0 ]; then
|
|
|
21
24
|
input=$(cat 2>/dev/null || true)
|
|
22
25
|
fi
|
|
23
26
|
|
|
24
|
-
# Extract file path from tool input
|
|
27
|
+
# Extract file path and content from tool input
|
|
25
28
|
file_path=""
|
|
29
|
+
content_to_write=""
|
|
26
30
|
if [ -n "$input" ] && command -v jq >/dev/null 2>&1; then
|
|
27
31
|
file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null || true)
|
|
32
|
+
# Write tool has 'content', Edit tool has 'new_string'
|
|
33
|
+
content_to_write=$(echo "$input" | jq -r '.tool_input.content // .tool_input.new_string // empty' 2>/dev/null || true)
|
|
28
34
|
fi
|
|
29
35
|
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
# SECRET DETECTION
|
|
38
|
+
# Scans the content being written for common credential patterns.
|
|
39
|
+
# Uses grep with POSIX ERE for speed (<5ms on typical file sizes).
|
|
40
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
if [ -n "$content_to_write" ]; then
|
|
42
|
+
SECRET_FOUND=""
|
|
43
|
+
SECRET_TYPE=""
|
|
44
|
+
|
|
45
|
+
# AWS access key: AKIA followed by 16 uppercase alphanumeric chars
|
|
46
|
+
if echo "$content_to_write" | grep -qE 'AKIA[0-9A-Z]{16}' 2>/dev/null; then
|
|
47
|
+
SECRET_FOUND=1
|
|
48
|
+
SECRET_TYPE="AWS access key (AKIA...)"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# OpenAI / Anthropic style API key: sk- followed by 48+ alphanumeric chars
|
|
52
|
+
if [ -z "$SECRET_FOUND" ] && echo "$content_to_write" | grep -qE 'sk-[a-zA-Z0-9]{48,}' 2>/dev/null; then
|
|
53
|
+
SECRET_FOUND=1
|
|
54
|
+
SECRET_TYPE="API secret key (sk-...)"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# PEM private key header
|
|
58
|
+
if [ -z "$SECRET_FOUND" ] && echo "$content_to_write" | \
|
|
59
|
+
grep -qE '-----BEGIN (RSA|EC|DSA|OPENSSH|PRIVATE) KEY-----' 2>/dev/null; then
|
|
60
|
+
SECRET_FOUND=1
|
|
61
|
+
SECRET_TYPE="PEM private key"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Generic password assignment in config-like contexts
|
|
65
|
+
# Matches: password=, PASSWORD=, "password": "...", passwd=
|
|
66
|
+
# Requires the value to be non-empty and at least 8 chars to avoid false positives
|
|
67
|
+
if [ -z "$SECRET_FOUND" ] && echo "$content_to_write" | \
|
|
68
|
+
grep -qiE '(password|passwd|secret|api_key|apikey)\s*[=:]\s*["\x27]?[A-Za-z0-9@#$%^&*!_\-]{8,}' 2>/dev/null; then
|
|
69
|
+
# Exclude known placeholder patterns: <password>, ${PASSWORD}, %(password)s, REPLACE_ME, etc.
|
|
70
|
+
if ! echo "$content_to_write" | \
|
|
71
|
+
grep -qiE '(password|passwd|secret|api_key|apikey)\s*[=:]\s*["\x27]?(<[^>]+>|\$\{[^}]+\}|%\([^)]+\)s|REPLACE_ME|YOUR_|TODO|CHANGEME|example|placeholder|xxx+|test)' 2>/dev/null; then
|
|
72
|
+
SECRET_FOUND=1
|
|
73
|
+
SECRET_TYPE="credential assignment (password/secret/api_key)"
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
if [ -n "$SECRET_FOUND" ]; then
|
|
78
|
+
if declare -f log_event >/dev/null 2>&1; then
|
|
79
|
+
log_event "secret_detected" "$(printf '{"file":"%s","type":"%s"}' \
|
|
80
|
+
"${file_path:-unknown}" "$SECRET_TYPE")"
|
|
81
|
+
fi
|
|
82
|
+
echo "SECRET DETECTED: ${SECRET_TYPE} found in write to '${file_path:-unknown}'. Remove the secret before committing." >&2
|
|
83
|
+
# Output block decision — Claude Code reads this JSON when exit code is 2
|
|
84
|
+
echo '{"decision":"block","reason":"Secret detected in file write. Remove the secret before committing."}'
|
|
85
|
+
exit 2
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
90
|
+
# SIGHTS CONSULTATION CHECK (advisory only)
|
|
91
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
92
|
+
if declare -f sights_was_checked_recently >/dev/null 2>&1; then
|
|
93
|
+
if ! sights_was_checked_recently 120; then
|
|
94
|
+
if declare -f log_event >/dev/null 2>&1; then
|
|
95
|
+
log_event "sights_skip_warning" "$(printf '{"file":"%s"}' "${file_path:-unknown}")"
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
33
98
|
fi
|
|
34
99
|
|
|
35
100
|
# Log the pre-edit event
|
|
36
|
-
log_event
|
|
101
|
+
if declare -f log_event >/dev/null 2>&1; then
|
|
102
|
+
log_event "pre_edit" "$(printf '{"file":"%s"}' "${file_path:-unknown}")"
|
|
103
|
+
fi
|
|
37
104
|
|
|
38
105
|
# Claude Code command hooks must output valid JSON to stdout
|
|
39
106
|
echo '{}'
|