chore(pwg): fail-closed persona-write-guard hook (#813) - #33
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds ChangesGit action guard
Estimated code review effort: 3 (Moderate) | ~15 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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-detection regex in the pwg-guard-v3 hook to
recognize git push and gh pr create when preceded by environment-variable
assignments, subshell opening parentheses, or equivalent command boundaries.
Extend gh matching to permit global options such as --repo before the pr
subcommand, while preserving existing command and branch checks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| "hooks": [ | ||
| { | ||
| "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", |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Multiple trivial regex bypasses defeat the guard.
The detection regex requires git/gh to be preceded by ^, ;, &, or |, but several common shell constructs place the command at a position the regex doesn't reach:
- Env-var prefix:
GIT_DIR=/tmp git pushorGH_REPO=owner/repo gh pr create— the=and preceding token don't match[;&|], so the command is not detected and the guard allows it. gh --repoflag:gh --repo owner/repo pr create— the regex requiresprimmediately aftergh[[:space:]]+, but--repo owner/reposits between them.- Subshell:
(git push)—(is not in[;&|], so the push is not detected.
These allow a no-persona session to push or open PRs, which is exactly what the guard is meant to prevent.
🔧 Proposed regex improvements
For the env-var prefix, add = to the boundary character class and allow word characters before git/gh:
-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;
+printf '%s' "$c" | grep -Eq '(^|[;&|=(]|[[:space:]]+)[[:space:]]*(git([[:space:]]+(-[Cc][[:space:]]+[^[:space:];&|]+|--[A-Za-z][A-Za-z-]*(=[^[:space:];&|]*)?|-[A-Za-z]+))*[[:space:]]+push|gh([[:space:]]+--[A-Za-z][A-Za-z-]*(=[^[:space:];&|]*)?[[:space:]]+[^[:space:];&|]*)*[[:space:]]+pr[[:space:]]+(create|merge))([[:space:];&|)]|$)' || exit 0;This adds = and ( to the left boundary set, and allows gh to be followed by global flags before pr.
Note: regex-based command detection is inherently bypassable (e.g., eval, bash -c, variable indirection). For a higher-assurance guard, consider parsing the command with a proper shell tokenizer or restricting the tool's allowed command patterns upstream.
📝 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.
| "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-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:]]+)[[:space:]]*(git([[:space:]]+(-[Cc][[:space:]]+[^[:space:];&|]+|--[A-Za-z][A-Za-z-]*(=[^[:space:];&|]*)?|-[A-Za-z]+))*[[:space:]]+push|gh([[:space:]]+--[A-Za-z][A-Za-z-]*(=[^[:space:];&|]*)?[[:space:]]+[^[:space:];&|]*)*[[: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", |
🤖 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-detection regex in the
pwg-guard-v3 hook to recognize git push and gh pr create when preceded by
environment-variable assignments, subshell opening parentheses, or equivalent
command boundaries. Extend gh matching to permit global options such as --repo
before the pr subcommand, while preserving existing command and branch checks.
Deploys the Persona-Write-Guard (#813 durable fix, dev-environment #67). Default-denies write verbs unless on a sanctioned
<persona>/…branch. Generated from dev-environmentlib/pwg.sh; merge-not-clobber into.claude/settings.json.Summary by CodeRabbit