-
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 1 commit
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,11 @@ 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. | ||
| NCOMMAND=$(echo "$COMMAND" | sed -E 's/(^|\s|&&\s*)git[[:space:]]+-C[[:space:]]+"[^"]+"/\1git/g; s/(^|\s|&&\s*)git[[:space:]]+-C[[:space:]]+[^[:space:]]+/\1git/g') | ||
|
|
||
| deny() { | ||
| local reason="$1" | ||
| node -e " | ||
|
|
@@ -43,46 +48,61 @@ 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: | ||
| # - `cd "<dir>" && git ...` → the cd target | ||
| # - `git -C "<dir>" ...` → the -C target | ||
| # Falls back to empty string (caller uses cwd). | ||
| detect_work_dir() { | ||
| local work_dir="" | ||
| if echo "$COMMAND" | grep -qE '^\s*cd\s+'; then | ||
| work_dir=$(echo "$COMMAND" | sed -nE 's/^\s*cd\s+"?([^"&]+)"?\s*&&.*/\1/p') | ||
| fi | ||
| if [ -z "$work_dir" ] && 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.
This combination is unusual in practice, but for completeness the detect_work_dir() {
local work_dir=""
# git -C takes explicit precedence over any ambient cd prefix
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
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
work_dir="${work_dir%"${work_dir##*[![:space:]]}"}"
echo "$work_dir"
}
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 — |
||
| # 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 +122,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 +155,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.
-Coptions leave NCOMMAND partially un-normalizedThe two
sedsubstitutions run sequentially on the same line. Each runs with/g, but after the first match replacesgit -C /path1withgit, the remainder of the string is-C /path2 push— there is no secondgittoken for the pattern to re-anchor on. Sogit -C /path1 -C /path2 push(two unquoted-Coptions) becomesgit -C /path2 pushinNCOMMAND, which does not matchgit\s+push, silently bypassing branch validation.A second pass of the same
sedexpression on the already-normalized string would collapse the leftover-Coption:git -Cwith multiple flags is rare in agent-generated commands, but closing the gap keeps the bypass surface minimal.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 — added a second sed pass so the
-Cnormalization re-anchors ongitafter the first replacement. Also tightened the unquoted pattern ([^"[:space:]][^[:space:]]*) so it no longer mis-matches the opening"of a quoted path on the second pass (which would have left a trailingpath"in NCOMMAND). Local smoke test coversgit -C /a -C /b push(unquoted) andgit -C "/p a/b" -C "/p c/d" push(quoted) — both collapse togit pushand matchgit\s+push.