From ff3b6b921af3dfa3e3132a887a70751ea05cc3c2 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Sun, 17 May 2026 17:41:24 -0600 Subject: [PATCH] fix(hooks): guard-git.sh sed patterns work on macOS BSD sed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `\s` and the `s///;t;s///` flow-control chain are GNU sed extensions that BSD sed (macOS default) does not support. When a command like `cd /path/to/wt && git push` was issued from a different cwd, the cd-path extraction silently returned an empty string, the hook fell back to the calling shell's branch, and produced false "Branch does not match required pattern" denials. Changes: - Replace `\s` with `[[:space:]]` in all sed patterns (lines 36, 37, 124). - Split the `s/quoted/.../p;t;s/unquoted/.../p` chain on the `-C` extraction into two separate `sed -nE` invocations — BSD sed parses the chained `s///` after `t` as a label and errors with "undefined label". - Update the matching `grep -qE` cd/-C guards to `[[:space:]]` for consistency. Verified end-to-end on macOS with all four code paths the hook protects (cd && push, -C push, -C with quoted path, multi -C, cd + -C precedence). Closes #1145 --- .claude/hooks/guard-git.sh | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.claude/hooks/guard-git.sh b/.claude/hooks/guard-git.sh index 46f96759..ba6c4ced 100644 --- a/.claude/hooks/guard-git.sh +++ b/.claude/hooks/guard-git.sh @@ -33,8 +33,8 @@ fi # the opening `"` of a quoted path (which would leave a trailing `path"` in # NCOMMAND). The pattern re-anchors on `git`, so multi-`-C` chains (e.g. # `git -C /a -C /b push`) need a second pass to collapse the residual `-C`. -NCOMMAND=$(echo "$COMMAND" | sed -E 's/(^|\s|&&\s*)git[[:space:]]+-C[[:space:]]+"[^"]+"/\1git/g; s/(^|\s|&&\s*)git[[:space:]]+-C[[:space:]]+[^"[:space:]][^[:space:]]*/\1git/g') -NCOMMAND=$(echo "$NCOMMAND" | sed -E 's/(^|\s|&&\s*)git[[:space:]]+-C[[:space:]]+"[^"]+"/\1git/g; s/(^|\s|&&\s*)git[[:space:]]+-C[[:space:]]+[^"[:space:]][^[:space:]]*/\1git/g') +NCOMMAND=$(echo "$COMMAND" | sed -E 's/(^|[[:space:]]|&&[[:space:]]*)git[[:space:]]+-C[[:space:]]+"[^"]+"/\1git/g; s/(^|[[:space:]]|&&[[:space:]]*)git[[:space:]]+-C[[:space:]]+[^"[:space:]][^[:space:]]*/\1git/g') +NCOMMAND=$(echo "$NCOMMAND" | sed -E 's/(^|[[:space:]]|&&[[:space:]]*)git[[:space:]]+-C[[:space:]]+"[^"]+"/\1git/g; s/(^|[[:space:]]|&&[[:space:]]*)git[[:space:]]+-C[[:space:]]+[^"[:space:]][^[:space:]]*/\1git/g') deny() { local reason="$1" @@ -117,11 +117,16 @@ detect_work_dir() { # `git -C` is the explicit git-level override and wins over any ambient cd prefix, # so check it first (e.g. `cd /tmp && git -C /worktree push` targets /worktree). # Greedy `.*-C` anchors on the LAST `-C` in the chosen segment. - if echo "$search_str" | grep -qE 'git\s+([^&|;]*\s)?-C\s+'; then - work_dir=$(echo "$search_str" | sed -nE 's/.*-C[[:space:]]+"([^"]+)".*/\1/p;t;s/.*-C[[:space:]]+([^[:space:]]+).*/\1/p') + # Two separate sed invocations (quoted path first, then unquoted fallback) instead + # of a single `;t;s` chain — BSD sed parses chained s/// after `t` as a label. + if echo "$search_str" | grep -qE 'git[[:space:]]+([^&|;]*[[:space:]])?-C[[:space:]]+'; then + work_dir=$(echo "$search_str" | sed -nE 's/.*-C[[:space:]]+"([^"]+)".*/\1/p') + if [ -z "$work_dir" ]; then + work_dir=$(echo "$search_str" | sed -nE 's/.*-C[[:space:]]+([^[:space:]]+).*/\1/p') + fi fi - if [ -z "$work_dir" ] && echo "$COMMAND" | grep -qE '^\s*cd\s+'; then - work_dir=$(echo "$COMMAND" | sed -nE 's/^\s*cd\s+"?([^"&]+)"?\s*&&.*/\1/p') + if [ -z "$work_dir" ] && echo "$COMMAND" | grep -qE '^[[:space:]]*cd[[:space:]]+'; then + work_dir=$(echo "$COMMAND" | sed -nE 's/^[[:space:]]*cd[[:space:]]+"?([^"&]+)"?[[:space:]]*&&.*/\1/p') fi # Trim trailing whitespace work_dir="${work_dir%"${work_dir##*[![:space:]]}"}"