Skip to content

chore(pwg): fail-closed persona-write-guard hook (#813) - #34

Merged
GmanFooFoo merged 1 commit into
mainfrom
bob/pwg-rollout-md-viewer
Jul 18, 2026
Merged

chore(pwg): fail-closed persona-write-guard hook (#813)#34
GmanFooFoo merged 1 commit into
mainfrom
bob/pwg-rollout-md-viewer

Conversation

@GmanFooFoo

@GmanFooFoo GmanFooFoo commented Jul 18, 2026

Copy link
Copy Markdown
Member

Deploys the Persona-Write-Guard (#813 durable fix, dev-environment #67). Default-denies write verbs unless on a sanctioned <persona>/… branch. Generated from dev-environment lib/pwg.sh; merge-not-clobber into .claude/settings.json.

Summary by CodeRabbit

  • Bug Fixes
    • Improved safeguards for ambiguous branch selections during checkout or branch switching.
    • Added protection against evaluating commands with unresolved directory targets.
    • Updated branch validation to prevent unauthorized push or pull request actions from unsupported sessions.

Adds the Persona-Write-Guard PreToolUse hook: default-denies git push /
gh pr create / gh pr merge unless the session is on a sanctioned <persona>/…
branch (AD-22). Generated + synced from dev-environment lib/pwg.sh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
md-viewer Ready Ready Preview, Comment Jul 18, 2026 4:32pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The Bash pre-tool hook in .claude/settings.json now uses pwg-guard-v5, adding branch ambiguity and unresolved git -C handling while narrowing the sanctioned persona allowlist.

Changes

PWG guard update

Layer / File(s) Summary
Bash guard enforcement
.claude/settings.json
The Bash hook changes from pwg-guard-v3 to pwg-guard-v5, adding PWG/#446 checks for ambiguous checkout targets and unresolved git -C variables, and removing AGENT-PREAMBLE from the sanctioned persona allowlist while retaining PWG/#813 enforcement.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: deploying a fail-closed persona-write-guard hook.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bob/pwg-rollout-md-viewer

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/settings.json:
- Line 9: Update the command guard’s checkout/switch target extraction in the
`ct`/`n` logic to recognize every branch-changing form, including checkout or
switch with an explicit branch target and long create options such as
`--create`, not only `-[bBc]`. Use the parsed target when validating the branch
before push or PR operations, and retain the existing ambiguous-target rejection
when multiple branch targets are detected.
- Line 9: The branch-target extraction in the command guard must not derive
checkout targets from arbitrary command text. Replace the text-wide
checkout/switch regex used to populate ct in the command hook with structural
parsing of actual git invocations, or fail closed when parsing is ambiguous;
ensure strings such as printf arguments cannot authorize a subsequent git push.
- Line 9: The command guard’s repository detection in the “command” setting only
captures the final git -C target, allowing earlier unauthorized pushes in
compound commands. Update the guard to detect all git -C targets and either
evaluate each write operation independently or reject commands containing
multiple repository targets before branch validation; preserve existing handling
for single targets and current-directory commands.
- Line 9: Update the `git -C` target resolution and branch-selection logic in
the command hook so an unexpanded or otherwise unresolved target exits nonzero
instead of setting `u=1`, warning, and falling back to the current directory’s
branch. Preserve normal branch validation for resolvable targets, but fail
closed before the sanctioned-branch check when the repository cannot be
identified.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 03594c79-7386-461f-be8b-f24dad918ef2

📥 Commits

Reviewing files that changed from the base of the PR and between 1f7d483 and b2011c6.

📒 Files selected for processing (1)
  • .claude/settings.json

Comment thread .claude/settings.json
{
"type": "command",
"command": ": pwg-guard-v3; i=$(cat); c=$(printf '%s' \"$i\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$c\" ] && c=$i; printf '%s' \"$c\" | grep -Eq '(^|[;&|])[[:space:]]*(git([[:space:]]+(-[Cc][[:space:]]+[^[:space:];&|]+|--[A-Za-z][A-Za-z-]*(=[^[:space:];&|]*)?|-[A-Za-z]+))*[[:space:]]+push|gh[[:space:]]+pr[[:space:]]+(create|merge))([[:space:];&|]|$)' || exit 0; d=''; t=$(printf '%s' \"$c\" | sed -n 's/.*git[[:space:]][[:space:]]*-C[[:space:]][[:space:]]*\\([^[:space:];&][^[:space:];&]*\\).*/\\1/p'); [ -n \"$t\" ] && d=$t; if [ -z \"$d\" ]; then p=$(printf '%s' \"$c\" | sed -n 's/^[[:space:]]*cd[[:space:]][[:space:]]*\\([^[:space:];&][^[:space:];&]*\\).*/\\1/p'); [ -n \"$p\" ] && d=$p; fi; case $d in '~') d=$HOME;; '~/'*) d=$HOME/${d#\\~/};; esac; if [ -n \"$d\" ]; then b=$(git -C \"$d\" rev-parse --abbrev-ref HEAD 2>/dev/null); else b=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); fi; printf '%s' \"$b\" | grep -Eqi '^(AGENT-PREAMBLE|bob|dia|dr-sommer|engels|gary|hans-cee|jack|james|lenin|linus|lorne-cee|maschin|obi|rumi|sensei|seo-[a-z-]+|steffi|sven|trillian)/' && exit 0; echo \"Write blocked (PWG/#813): branch '$b' is not a sanctioned <persona>/… branch. A no-persona/cloud session must PROPOSE, not push or open PRs.\" >&2; exit 2",
"command": ": pwg-guard-v5; i=$(cat); c=$(printf '%s' \"$i\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$c\" ] && c=$i; k=$(printf '%s' \"$c\" | awk 'BEGIN{h=0} h==1{if($0 ~ \"^[[:blank:]]*\" d \"[[:blank:]]*$\"){h=0} next} {if(match($0,/<<-?[[:blank:]]*[\\047\\042]?[A-Za-z_][A-Za-z0-9_]*[\\047\\042]?/)){s=substr($0,RSTART,RLENGTH);sub(/^<<-?[[:blank:]]*/,\"\",s);gsub(/[\\047\\042]/,\"\",s);d=s;h=1} print}'); printf '%s' \"$k\" | grep -Eq '(^|[;&|])[[:space:]]*(git([[:space:]]+(-[Cc][[:space:]]+[^[:space:];&|]+|--[A-Za-z][A-Za-z-]*(=[^[:space:];&|]*)?|-[A-Za-z]+))*[[:space:]]+push|gh[[:space:]]+pr[[:space:]]+(create|merge))([[:space:];&|]|$)' || exit 0; ct=$(printf '%s' \"$k\" | grep -Eo '(checkout|switch)[[:space:]]+(-[qf][[:space:]]+)*-[bBc][[:space:]]+[^[:space:];&|]+' | sed -E 's/.*[[:space:]]//' | grep -v '^$' | sort -u); n=$(printf '%s' \"$ct\" | grep -c .); [ \"$n\" -gt 1 ] && { echo \"Write blocked (PWG/#446): ambiguous branch target — this command checks out more than one branch, so the guard cannot tell which one is pushed. Split the calls.\" >&2; exit 2; }; d=''; u=0; t=$(printf '%s' \"$k\" | sed -n 's/.*git[[:space:]][[:space:]]*-C[[:space:]][[:space:]]*\\([^[:space:];&][^[:space:];&]*\\).*/\\1/p'); [ -n \"$t\" ] && d=$t; if [ -z \"$d\" ]; then p=$(printf '%s' \"$k\" | sed -n 's/^[[:space:]]*cd[[:space:]][[:space:]]*\\([^[:space:];&][^[:space:];&]*\\).*/\\1/p'); [ -n \"$p\" ] && d=$p; fi; case $d in *'$'*) u=1; d='';; esac; case $d in '~') d=$HOME;; '~/'*) d=$HOME/${d#\\~/};; esac; if [ \"$n\" -eq 1 ]; then b=$ct; elif [ -n \"$d\" ]; then b=$(git -C \"$d\" rev-parse --abbrev-ref HEAD 2>/dev/null); else b=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); fi; [ \"$u\" -eq 1 ] && echo \"PWG/#446: the -C target is an unexpanded variable and cannot be resolved; judging against the current-directory branch '$b' instead.\" >&2; printf '%s' \"$b\" | grep -Eqi '^(bob|dia|dr-sommer|engels|gary|hans-cee|jack|james|lenin|linus|lorne-cee|maschin|obi|rumi|sensei|seo-[a-z-]+|steffi|sven|trillian)/' && exit 0; echo \"Write blocked (PWG/#813): branch '$b' is not a sanctioned <persona>/… branch. A no-persona/cloud session must PROPOSE, not push or open PRs.\" >&2; exit 2",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Validate every checkout/switch target, not only branch-creation flags.

The ct pattern only recognizes checkout/switch with -[bBc]. From a sanctioned branch, git checkout untrusted && git push or git switch --create untrusted && git push is therefore judged against the pre-command sanctioned branch and allowed. Parse all branch-changing forms and reject ambiguous targets.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/settings.json at line 9, Update the command guard’s checkout/switch
target extraction in the `ct`/`n` logic to recognize every branch-changing form,
including checkout or switch with an explicit branch target and long create
options such as `--create`, not only `-[bBc]`. Use the parsed target when
validating the branch before push or PR operations, and retain the existing
ambiguous-target rejection when multiple branch targets are detected.

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Do not infer checkout targets from arbitrary command text.

The regex scans the entire command rather than actual git invocations. For example, printf 'checkout -b bob/x'; git push produces ct=bob/x and can authorize a push without performing any checkout. Parse command arguments structurally, or fail closed whenever target extraction is ambiguous.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/settings.json at line 9, The branch-target extraction in the command
guard must not derive checkout targets from arbitrary command text. Replace the
text-wide checkout/switch regex used to populate ct in the command hook with
structural parsing of actual git invocations, or fail closed when parsing is
ambiguous; ensure strings such as printf arguments cannot authorize a subsequent
git push.

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Reject compound writes with multiple git -C targets.

The greedy sed expression assigns only the final git -C path. A command such as git -C /untrusted push; git -C /sanctioned push is judged solely against /sanctioned, allowing the unauthorized first write. Validate each write operation independently or block when multiple repository targets are present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/settings.json at line 9, The command guard’s repository detection in
the “command” setting only captures the final git -C target, allowing earlier
unauthorized pushes in compound commands. Update the guard to detect all git -C
targets and either evaluate each write operation independently or reject
commands containing multiple repository targets before branch validation;
preserve existing handling for single targets and current-directory commands.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Fail closed when git -C cannot be resolved.

When git -C "$OTHER_REPO" push runs from a sanctioned current directory, u=1 only emits a warning, then the hook judges the current directory’s sanctioned branch and allows the write to an unknown repository. Exit nonzero when the target is unresolved instead of falling back to an unrelated branch.

Proposed fix
- [ "$u" -eq 1 ] && echo "PWG/#446: the -C target is an unexpanded variable and cannot be resolved; judging against the current-directory branch '$b' instead." >&2;
+ [ "$u" -eq 1 ] && { echo "Write blocked (PWG/#446): the -C target is an unexpanded variable and cannot be resolved." >&2; exit 2; };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"command": ": pwg-guard-v5; i=$(cat); c=$(printf '%s' \"$i\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$c\" ] && c=$i; k=$(printf '%s' \"$c\" | awk 'BEGIN{h=0} h==1{if($0 ~ \"^[[:blank:]]*\" d \"[[:blank:]]*$\"){h=0} next} {if(match($0,/<<-?[[:blank:]]*[\\047\\042]?[A-Za-z_][A-Za-z0-9_]*[\\047\\042]?/)){s=substr($0,RSTART,RLENGTH);sub(/^<<-?[[:blank:]]*/,\"\",s);gsub(/[\\047\\042]/,\"\",s);d=s;h=1} print}'); printf '%s' \"$k\" | grep -Eq '(^|[;&|])[[:space:]]*(git([[:space:]]+(-[Cc][[:space:]]+[^[:space:];&|]+|--[A-Za-z][A-Za-z-]*(=[^[:space:];&|]*)?|-[A-Za-z]+))*[[:space:]]+push|gh[[:space:]]+pr[[:space:]]+(create|merge))([[:space:];&|]|$)' || exit 0; ct=$(printf '%s' \"$k\" | grep -Eo '(checkout|switch)[[:space:]]+(-[qf][[:space:]]+)*-[bBc][[:space:]]+[^[:space:];&|]+' | sed -E 's/.*[[:space:]]//' | grep -v '^$' | sort -u); n=$(printf '%s' \"$ct\" | grep -c .); [ \"$n\" -gt 1 ] && { echo \"Write blocked (PWG/#446): ambiguous branch target — this command checks out more than one branch, so the guard cannot tell which one is pushed. Split the calls.\" >&2; exit 2; }; d=''; u=0; t=$(printf '%s' \"$k\" | sed -n 's/.*git[[:space:]][[:space:]]*-C[[:space:]][[:space:]]*\\([^[:space:];&][^[:space:];&]*\\).*/\\1/p'); [ -n \"$t\" ] && d=$t; if [ -z \"$d\" ]; then p=$(printf '%s' \"$k\" | sed -n 's/^[[:space:]]*cd[[:space:]][[:space:]]*\\([^[:space:];&][^[:space:];&]*\\).*/\\1/p'); [ -n \"$p\" ] && d=$p; fi; case $d in *'$'*) u=1; d='';; esac; case $d in '~') d=$HOME;; '~/'*) d=$HOME/${d#\\~/};; esac; if [ \"$n\" -eq 1 ]; then b=$ct; elif [ -n \"$d\" ]; then b=$(git -C \"$d\" rev-parse --abbrev-ref HEAD 2>/dev/null); else b=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); fi; [ \"$u\" -eq 1 ] && echo \"PWG/#446: the -C target is an unexpanded variable and cannot be resolved; judging against the current-directory branch '$b' instead.\" >&2; printf '%s' \"$b\" | grep -Eqi '^(bob|dia|dr-sommer|engels|gary|hans-cee|jack|james|lenin|linus|lorne-cee|maschin|obi|rumi|sensei|seo-[a-z-]+|steffi|sven|trillian)/' && exit 0; echo \"Write blocked (PWG/#813): branch '$b' is not a sanctioned <persona>/… branch. A no-persona/cloud session must PROPOSE, not push or open PRs.\" >&2; exit 2",
"command": ": pwg-guard-v5; i=$(cat); c=$(printf '%s' \"$i\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$c\" ] && c=$i; k=$(printf '%s' \"$c\" | awk 'BEGIN{h=0} h==1{if($0 ~ \"^[[:blank:]]*\" d \"[[:blank:]]*$\"){h=0} next} {if(match($0,/<<-?[[:blank:]]*[\\047\\042]?[A-Za-z_][A-Za-z0-9_]*[\\047\\042]?/)){s=substr($0,RSTART,RLENGTH);sub(/^<<-?[[:blank:]]*/,\"\",s);gsub(/[\\047\\042]/,\"\",s);d=s;h=1} print}'); printf '%s' \"$k\" | grep -Eq '(^|[;&|])[[:space:]]*(git([[:space:]]+(-[Cc][[:space:]]+[^[:space:];&|]+|--[A-Za-z][A-Za-z-]*(=[^[:space:];&|]*)?|-[A-Za-z]+))*[[:space:]]+push|gh[[:space:]]+pr[[:space:]]+(create|merge))([[:space:];&|]|$)' || exit 0; ct=$(printf '%s' \"$k\" | grep -Eo '(checkout|switch)[[:space:]]+(-[qf][[:space:]]+)*-[bBc][[:space:]]+[^[:space:];&|]+' | sed -E 's/.*[[:space:]]//' | grep -v '^$' | sort -u); n=$(printf '%s' \"$ct\" | grep -c .); [ \"$n\" -gt 1 ] && { echo \"Write blocked (PWG/#446): ambiguous branch target — this command checks out more than one branch, so the guard cannot tell which one is pushed. Split the calls.\" >&2; exit 2; }; d=''; u=0; t=$(printf '%s' \"$k\" | sed -n 's/.*git[[:space:]][[:space:]]*-C[[:space:]][[:space:]]*\\([^[:space:];&][^[:space:];&]*\\).*/\\1/p'); [ -n \"$t\" ] && d=$t; if [ -z \"$d\" ]; then p=$(printf '%s' \"$k\" | sed -n 's/^[[:space:]]*cd[[:space:]][[:space:]]*\\([^[:space:];&][^[:space:];&]*\\).*/\\1/p'); [ -n \"$p\" ] && d=$p; fi; case $d in *'$'*) u=1; d='';; esac; case $d in '~') d=$HOME;; '~/'*) d=$HOME/${d#\\~/};; esac; if [ \"$n\" -eq 1 ]; then b=$ct; elif [ -n \"$d\" ]; then b=$(git -C \"$d\" rev-parse --abbrev-ref HEAD 2>/dev/null); else b=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); fi; [ \"$u\" -eq 1 ] && { echo \"Write blocked (PWG/#446): the -C target is an unexpanded variable and cannot be resolved.\" >&2; exit 2; }; printf '%s' \"$b\" | grep -Eqi '^(bob|dia|dr-sommer|engels|gary|hans-cee|jack|james|lenin|linus|lorne-cee|maschin|obi|rumi|sensei|seo-[a-z-]+|steffi|sven|trillian)/' && exit 0; echo \"Write blocked (PWG/#813): branch '$b' is not a sanctioned <persona>/… branch. A no-persona/cloud session must PROPOSE, not push or open PRs.\" >&2; exit 2",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/settings.json at line 9, Update the `git -C` target resolution and
branch-selection logic in the command hook so an unexpanded or otherwise
unresolved target exits nonzero instead of setting `u=1`, warning, and falling
back to the current directory’s branch. Preserve normal branch validation for
resolvable targets, but fail closed before the sanctioned-branch check when the
repository cannot be identified.

@GmanFooFoo
GmanFooFoo merged commit 9a4c97f into main Jul 18, 2026
6 of 7 checks passed
@GmanFooFoo
GmanFooFoo deleted the bob/pwg-rollout-md-viewer branch July 18, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant