Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"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",
"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.

"timeout": 10
}
]
Expand Down
Loading