chore(process): #303 H7 worktree-owner guard + complete Wave 1 wiring
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m1s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m13s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m1s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m13s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Wave 2 hook H7: never commit/merge inside a sibling worktree another session created (burned us on #289 path-leak + the plumbing-merge workaround). Ownership = a per-session .claude-worktree-owner marker: - posttooluse-worktree-marker.sh stamps a worktree with session_id on `git worktree add` (parses the <path> arg past -b/-B/--reason flags). - pretooluse-worktree-guard.sh denies `git commit`/`git merge` whose effective dir (resolves `git -C <p>` and leading `cd <p> &&`) is a worktree whose marker names a DIFFERENT session. Fail-open: no marker, unparsable, or own session -> allow. Main tree + pre-convention worktrees are never marked, so unaffected. Also completes Wave 1's rollout, which committed pretooluse-bash-guard.sh but left .claude/settings.json and the agent-ram/nav-guard hooks untracked (so nothing was actually wired). Adds the settings.json that registers all five hooks (PreToolUse Bash x2, nav, Agent; PostToolUse Bash) + the .gitignore worktree-marker line, screenshot-scratch rules, and the decisions.md TOC left uncommitted last session. All hooks pipe-tested (7 guard cases + 6 marker cases). Refs #303. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostToolUse / Bash — after a successful `git worktree add`, stamp the new worktree with
|
||||
# this session's id (.claude-worktree-owner) so pretooluse-worktree-guard.sh (H7) can tell
|
||||
# a sibling worktree another session created apart from this session's own.
|
||||
# Fail-safe: any parse trouble → do nothing (the guard stays fail-open without a marker).
|
||||
set -euo pipefail
|
||||
input=$(cat)
|
||||
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""' 2>/dev/null || true)
|
||||
cwd=$(printf '%s' "$input" | jq -r '.cwd // ""' 2>/dev/null || true)
|
||||
me=$(printf '%s' "$input" | jq -r '.session_id // ""' 2>/dev/null || true)
|
||||
|
||||
printf '%s' "$cmd" | grep -qE 'git[[:space:]]+worktree[[:space:]]+add\b' || exit 0
|
||||
[ -z "$me" ] && exit 0
|
||||
[ -z "$cwd" ] && cwd="$PWD"
|
||||
|
||||
# Extract the <path> arg of `git worktree add [flags] <path> [<commit-ish>]`.
|
||||
# Skip flags; skip the values of the value-taking flags (-b/-B/--reason). Worktree paths
|
||||
# in this repo have no spaces, so whitespace tokenization is safe.
|
||||
add_args=$(printf '%s' "$cmd" | sed -E 's/.*git[[:space:]]+worktree[[:space:]]+add[[:space:]]+//')
|
||||
path=""
|
||||
skip=0
|
||||
for tok in $add_args; do
|
||||
if [ "$skip" = 1 ]; then skip=0; continue; fi
|
||||
case "$tok" in
|
||||
-b|-B|--reason) skip=1; continue ;;
|
||||
--) continue ;;
|
||||
-*) continue ;;
|
||||
*) path=$(printf '%s' "$tok" | tr -d '"'"'"''); break ;;
|
||||
esac
|
||||
done
|
||||
[ -z "$path" ] && exit 0
|
||||
case "$path" in /*) abs="$path" ;; *) abs="$cwd/$path" ;; esac
|
||||
[ -d "$abs" ] || exit 0
|
||||
# Don't clobber a marker a different session already planted.
|
||||
[ -f "$abs/.claude-worktree-owner" ] && exit 0
|
||||
printf '%s\n' "$me" > "$abs/.claude-worktree-owner" 2>/dev/null || true
|
||||
exit 0
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse / Agent (subagent spawn) — RAM-gate the fan-out.
|
||||
# The historic 8-9-way crash was RAM starvation, not CPU load; gate on FREE RAM.
|
||||
# Fail-open: if memory_pressure is unavailable/unparsable → allow.
|
||||
set -euo pipefail
|
||||
free=$(memory_pressure -Q 2>/dev/null | grep -oE 'free percentage: [0-9]+' | grep -oE '[0-9]+' || true)
|
||||
[ -z "${free:-}" ] && exit 0
|
||||
|
||||
if [ "$free" -lt 10 ]; then
|
||||
jq -n --arg f "$free" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:("Free RAM \($f)% (<10%): do NOT spawn more agents — the historic crash was RAM starvation from an 8-9-way fan-out. Wait for memory_pressure -Q to recover, then retry.")}}'
|
||||
elif [ "$free" -lt 20 ]; then
|
||||
jq -n --arg f "$free" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"ask",permissionDecisionReason:("Free RAM \($f)% (<20%): near the fan-out ceiling. Confirm before adding another build/implementer agent (read-only recon agents are cheap).")}}'
|
||||
fi
|
||||
exit 0
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse / browser-navigate — deny opening download/stream endpoints in a tab
|
||||
# (they hang the MCP session; curl them instead). Fail-open on parse trouble.
|
||||
set -euo pipefail
|
||||
input=$(cat)
|
||||
url=$(printf '%s' "$input" | jq -r '.tool_input.url // ""' 2>/dev/null || true)
|
||||
|
||||
if printf '%s' "$url" | grep -qE '/iptv/|\.m3u8|/artwork/|playback\.m3u8'; then
|
||||
jq -n '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:"Blocked: do not open download/stream endpoints (/iptv, .m3u8, /artwork, playback.m3u8) in a browser tab — they stall the MCP session. curl them instead (docs/handoffs lore)."}}'
|
||||
fi
|
||||
exit 0
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse / Bash — deny `git commit`/`git merge` inside a sibling worktree that
|
||||
# a DIFFERENT session created (burned us twice — #289 path-leak, the plumbing-merge
|
||||
# workaround exists precisely because of this). Ownership is a `.claude-worktree-owner`
|
||||
# marker (session id) written at `git worktree add` time by posttooluse-worktree-marker.sh.
|
||||
#
|
||||
# Fail-open by design: no marker, unparsable input, or marker == this session → allow.
|
||||
# So the main tree (never marked) and pre-convention worktrees (no marker) are unaffected;
|
||||
# only a commit/merge into another session's marked worktree is blocked.
|
||||
set -euo pipefail
|
||||
input=$(cat)
|
||||
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""' 2>/dev/null || true)
|
||||
cwd=$(printf '%s' "$input" | jq -r '.cwd // ""' 2>/dev/null || true)
|
||||
me=$(printf '%s' "$input" | jq -r '.session_id // ""' 2>/dev/null || true)
|
||||
|
||||
# Only guard the state-mutating ops. Match `git commit`/`git merge` in command position
|
||||
# (line start or after a shell separator) so a quoted mention never false-trips.
|
||||
printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*git[[:space:]]+(-C[[:space:]]+[^[:space:]]+[[:space:]]+)?(commit|merge)\b' || exit 0
|
||||
|
||||
[ -z "$cwd" ] && cwd="$PWD"
|
||||
|
||||
# Determine the effective directory the git op runs in. Two common redirections in the
|
||||
# lore's usage move it off the session cwd: `git -C <path>` and a leading `cd <path> &&`.
|
||||
effdir="$cwd"
|
||||
cpath=$(printf '%s' "$cmd" | grep -oE 'git[[:space:]]+-C[[:space:]]+[^[:space:]&|;]+' | head -1 | sed -E 's/^git[[:space:]]+-C[[:space:]]+//' | tr -d '"'"'"'' || true)
|
||||
cdpath=$(printf '%s' "$cmd" | grep -oE '^[[:space:]]*cd[[:space:]]+[^[:space:]&|;]+' | head -1 | sed -E 's/^[[:space:]]*cd[[:space:]]+//' | tr -d '"'"'"'' || true)
|
||||
if [ -n "${cpath:-}" ]; then
|
||||
effdir="$cpath"
|
||||
elif [ -n "${cdpath:-}" ]; then
|
||||
effdir="$cdpath"
|
||||
fi
|
||||
# Resolve a relative effective dir against the session cwd.
|
||||
case "$effdir" in /*) : ;; *) effdir="$cwd/$effdir" ;; esac
|
||||
|
||||
root=$(git -C "$effdir" rev-parse --show-toplevel 2>/dev/null || true)
|
||||
[ -z "$root" ] && exit 0
|
||||
marker="$root/.claude-worktree-owner"
|
||||
[ -f "$marker" ] || exit 0
|
||||
owner=$(tr -d '[:space:]' < "$marker" 2>/dev/null || true)
|
||||
[ -z "$owner" ] && exit 0
|
||||
[ "$owner" = "$me" ] && exit 0
|
||||
|
||||
# Marker names a DIFFERENT session → deny.
|
||||
jq -n --arg o "$owner" --arg r "$root" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:("Blocked: worktree \($r) is owned by session \($o), not this one. Never commit/merge inside a sibling worktree another session created (#289 path-leak, plumbing-merge workaround). Commit from your own tree; if you genuinely own this worktree now, overwrite its .claude-worktree-owner marker with your session id.")}}'
|
||||
exit 0
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/pretooluse-bash-guard.sh\"",
|
||||
"timeout": 10
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/pretooluse-worktree-guard.sh\"",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__plugin_playwright_playwright__browser_navigate|mcp__claude-in-chrome__navigate",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/pretooluse-nav-guard.sh\"",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Agent|Task",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/pretooluse-agent-ram.sh\"",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/posttooluse-worktree-marker.sh\"",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user