-
Notifications
You must be signed in to change notification settings - Fork 10
fix(hooks): recognize git -C <path> in guard-git.sh
#1004
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
deb916c
2e290cb
10c50fe
4ec7187
669bf0e
fa18f2e
1039447
51210cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,16 @@ if ! echo "$COMMAND" | grep -qE '(^|\s|&&\s*)(git|gh)\s+'; then | |
| exit 0 | ||
| fi | ||
|
|
||
| # Normalize: strip `git -C "<path>"` / `git -C <path>` so downstream subcommand | ||
| # patterns (git\s+push, git\s+commit, …) match regardless of whether `-C` is | ||
| # present. detect_work_dir still inspects the raw $COMMAND to find the target. | ||
| # The unquoted pattern requires a non-quote first char so it does not mis-match | ||
| # 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') | ||
|
|
||
| deny() { | ||
| local reason="$1" | ||
| node -e " | ||
|
|
@@ -43,46 +53,63 @@ deny() { | |
| # --- Block dangerous commands --- | ||
|
|
||
| # git add . / git add -A / git add --all (broad staging) | ||
| if echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+add\s+(\.\s*$|-A|--all)'; then | ||
| if echo "$NCOMMAND" | grep -qE '(^|\s|&&\s*)git\s+add\s+(\.\s*$|-A|--all)'; then | ||
| deny "BLOCKED: 'git add .' / 'git add -A' stages ALL changes including other sessions' work. Stage specific files instead: git add <file1> <file2>" | ||
| fi | ||
|
|
||
| # git reset (unstaging / hard reset) | ||
| if echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+reset'; then | ||
| if echo "$NCOMMAND" | grep -qE '(^|\s|&&\s*)git\s+reset'; then | ||
| deny "BLOCKED: 'git reset' can unstage or destroy other sessions' work. To unstage your own files, use: git restore --staged <file>" | ||
| fi | ||
|
|
||
| # git checkout -- <file> (reverting files) | ||
| if echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+checkout\s+--'; then | ||
| if echo "$NCOMMAND" | grep -qE '(^|\s|&&\s*)git\s+checkout\s+--'; then | ||
| deny "BLOCKED: 'git checkout -- <file>' reverts working tree changes and may destroy other sessions' edits. If you need to discard your own changes, be explicit about which files." | ||
| fi | ||
|
|
||
| # git restore (reverting) — EXCEPT git restore --staged (safe unstaging) | ||
| if echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+restore'; then | ||
| if ! echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+restore\s+--staged'; then | ||
| if echo "$NCOMMAND" | grep -qE '(^|\s|&&\s*)git\s+restore'; then | ||
| if ! echo "$NCOMMAND" | grep -qE '(^|\s|&&\s*)git\s+restore\s+--staged'; then | ||
| deny "BLOCKED: 'git restore <file>' reverts working tree changes and may destroy other sessions' edits. To unstage files safely, use: git restore --staged <file>" | ||
| fi | ||
| fi | ||
|
|
||
| # git clean (delete untracked files) | ||
| if echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+clean'; then | ||
| if echo "$NCOMMAND" | grep -qE '(^|\s|&&\s*)git\s+clean'; then | ||
| deny "BLOCKED: 'git clean' deletes untracked files that may belong to other sessions." | ||
| fi | ||
|
|
||
| # git stash (hides all changes) | ||
| if echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+stash'; then | ||
| if echo "$NCOMMAND" | grep -qE '(^|\s|&&\s*)git\s+stash'; then | ||
| deny "BLOCKED: 'git stash' hides all working tree changes including other sessions' work. In worktree mode, commit your changes directly instead." | ||
| fi | ||
|
|
||
| # --- Branch name validation helper --- | ||
| # --- Working directory detection --- | ||
|
|
||
| validate_branch_name() { | ||
| # Try to get branch from the working directory where the command runs | ||
| # Extract cd target if command starts with cd "..." && ... | ||
| # Resolve the working directory a git command targets: | ||
| # - `git -C "<dir>" ...` → the -C target (takes precedence — explicit git-level override) | ||
| # - `cd "<dir>" && git ...` → the cd target | ||
| # Falls back to empty string (caller uses cwd). | ||
| detect_work_dir() { | ||
| local work_dir="" | ||
| if echo "$COMMAND" | grep -qE '^\s*cd\s+'; then | ||
| # `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). | ||
| if echo "$COMMAND" | grep -qE 'git\s+-C\s+'; then | ||
| work_dir=$(echo "$COMMAND" | sed -nE 's/.*git[[:space:]]+-C[[:space:]]+"([^"]+)".*/\1/p;t;s/.*git[[:space:]]+-C[[:space:]]+([^[:space:]]+).*/\1/p') | ||
| fi | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The greedy NCOMMAND collapses the chain to A one-pass strip of NCOMMAND-style normalization removes the first if echo "$COMMAND" | grep -qE 'git\s+-C\s+'; then
local cmd_last_c
cmd_last_c=$(echo "$COMMAND" | \
sed -E 's/(^|\s|&&\s*)git[[:space:]]+-C[[:space:]]+"[^"]+"/\1git/g; s/(^|\s|&&\s*)git[[:space:]]+-C[[:space:]]+[^"[:space:]][^[:space:]]*/\1git/g')
if echo "$cmd_last_c" | grep -qE 'git\s+-C\s+'; then
work_dir=$(echo "$cmd_last_c" | sed -nE \
's/.*git[[:space:]]+-C[[:space:]]+"([^"]+)".*/\1/p;t;s/.*git[[:space:]]+-C[[:space:]]+([^[:space:]]+).*/\1/p')
else
work_dir=$(echo "$COMMAND" | sed -nE \
's/.*git[[:space:]]+-C[[:space:]]+"([^"]+)".*/\1/p;t;s/.*git[[:space:]]+-C[[:space:]]+([^[:space:]]+).*/\1/p')
fi
fiThis keeps a single-
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — |
||
| 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') | ||
| fi | ||
| # Trim trailing whitespace | ||
| work_dir="${work_dir%"${work_dir##*[![:space:]]}"}" | ||
| echo "$work_dir" | ||
| } | ||
|
|
||
| # --- Branch name validation helper --- | ||
|
|
||
| validate_branch_name() { | ||
| local work_dir | ||
| work_dir=$(detect_work_dir) | ||
|
|
||
| local BRANCH="" | ||
| if [ -n "$work_dir" ] && [ -d "$work_dir" ]; then | ||
|
|
@@ -102,21 +129,29 @@ validate_branch_name() { | |
|
|
||
| # --- Branch name validation on push --- | ||
|
|
||
| if echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+push'; then | ||
| if echo "$NCOMMAND" | grep -qE '(^|\s|&&\s*)git\s+push'; then | ||
| validate_branch_name | ||
| fi | ||
|
|
||
| # --- Branch name validation on gh pr create --- | ||
|
|
||
| if echo "$COMMAND" | grep -qE '(^|\s|&&\s*)gh\s+pr\s+create'; then | ||
| if echo "$NCOMMAND" | grep -qE '(^|\s|&&\s*)gh\s+pr\s+create'; then | ||
| validate_branch_name | ||
| fi | ||
|
|
||
| # --- Commit validation against edit log --- | ||
|
|
||
| if echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+commit'; then | ||
| # Use git worktree root so each worktree session has its own edit log | ||
| PROJECT_DIR=$(git rev-parse --show-toplevel 2>/dev/null) || PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}" | ||
| if echo "$NCOMMAND" | grep -qE '(^|\s|&&\s*)git\s+commit'; then | ||
| # Resolve the target worktree so the edit log and staged-file listing come | ||
| # from the same repo the commit targets (e.g. `git -C <pr-worktree> commit`). | ||
| WORK_DIR=$(detect_work_dir) | ||
| if [ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ]; then | ||
| PROJECT_DIR=$(git -C "$WORK_DIR" rev-parse --show-toplevel 2>/dev/null) || PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}" | ||
| STAGED_FILES=$(git -C "$WORK_DIR" diff --cached --name-only 2>/dev/null) || true | ||
| else | ||
| PROJECT_DIR=$(git rev-parse --show-toplevel 2>/dev/null) || PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}" | ||
| STAGED_FILES=$(git diff --cached --name-only 2>/dev/null) || true | ||
| fi | ||
| LOG_FILE="$PROJECT_DIR/.claude/session-edits.log" | ||
|
|
||
| # If no edit log exists, allow (backward compat for sessions without tracking) | ||
|
|
@@ -127,9 +162,6 @@ if echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+commit'; then | |
| # Get unique edited files from log | ||
| EDITED_FILES=$(awk '{print $2}' "$LOG_FILE" | sort -u) | ||
|
|
||
| # Get staged files | ||
| STAGED_FILES=$(git diff --cached --name-only 2>/dev/null) || true | ||
|
|
||
| if [ -z "$STAGED_FILES" ]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
detect_work_dirtargets the lastgit -Cin a chained command, bypassing push branch validationThe
sedpattern uses a greedy.*gitanchor, so it always extracts the-Cpath from the lastgittoken in the string. For a chained command likegit -C /wt-bad-branch push && git -C /wt-good-branch commit -m "msg", both the push and commit checks fire onNCOMMAND, butdetect_work_dirreturns/wt-good-branch(from the trailingcommit).validate_branch_namethen reads the HEAD of the wrong worktree and approves the push — a branch-validation bypass whenever the last sub-command targets a conforming worktree.validate_branch_nameneeds the-Cpath of the first git token (the one driving the push), while the commit-validation block needs the last. One approach is to pass a hint todetect_work_dirso each caller can anchor to the right sub-command, or strip all sub-commands after the first&&before callingdetect_work_dirfrom withinvalidate_branch_name.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed —
detect_work_dirnow takes a subcommand hint (push,commit) and narrows to the&&-separated segment whose git invocation runs that subcommand before extracting-C. Forgit -C /wt-bad push && git -C /wt-good commit -m "msg", the push block callsvalidate_branch_name pushwhich resolves to/wt-bad(correctly blocking the non-conforming branch), and the commit block callsdetect_work_dir commitwhich resolves to/wt-goodfor the edit-log check. Regression test suite covers this exact chained scenario and confirms push/commit each see their own worktree.