🤖 Generated by the Agentic Engineer
Evidence
forge-readonly-guard.sh consumes a value-taking flag's next word before that word is checked against the read-only allowlist. So a denied flag placed immediately after any value flag is never classified at all.
Measured on origin/main (unmodified guard, f8da2a50's base):
rc=0 gh pr list --repo --web :: allow
rc=0 gh pr list --state --web :: allow
rc=0 gh pr view 1 --limit --web :: allow
--web is precisely the flag the guard's own comments name as the reason read verbs are allowlisted at all:
gh pr view --web opens a URL through $BROWSER, which runs a local program the guard never classified.
The same shape applies to every entry in GH_VERB_VALUE_FLAGS and GH_API_VALUE_FLAGS.
Why this is a Bug and not Security
It is not exploitable as written, and I would rather say so than inflate the Security queue. Real gh uses pflag, which parses --web as the value of --repo and then errors on the malformed repo argument — no browser opens. The defect is that the guard's safety currently depends on the downstream parser rejecting it, rather than on the guard's own classification. That is a latent correctness gap in an enforcement control: it holds today for a reason the guard does not itself assert, and it would stop holding if any value flag ever gained a permissive value grammar.
Expected behaviour
A word consumed as a flag value should still be rejected when it is itself a denied flag — or the guard should assert explicitly why consuming it is safe for that specific flag.
Note the tension to resolve rather than paper over: some legitimate values genuinely begin with - (a --search expression such as -label:foo, a --jq program). So this is not a blanket "reject dash-leading values" — it needs a deliberate rule, which is why it is filed rather than patched inline.
Acceptance criteria
Rough size: S–M — one classification site, but the value-grammar question needs deciding per flag family.
Provenance
Found while fixing the P1 on #177 (the guard denying the mandated bare --json vocabulary probe). Reported rather than folded into that PR to keep it one concern; #177's fix neither introduces nor widens this.
Evidence
forge-readonly-guard.shconsumes a value-taking flag's next word before that word is checked against the read-only allowlist. So a denied flag placed immediately after any value flag is never classified at all.Measured on
origin/main(unmodified guard,f8da2a50's base):--webis precisely the flag the guard's own comments name as the reason read verbs are allowlisted at all:The same shape applies to every entry in
GH_VERB_VALUE_FLAGSandGH_API_VALUE_FLAGS.Why this is a Bug and not Security
It is not exploitable as written, and I would rather say so than inflate the Security queue. Real
ghuses pflag, which parses--webas the value of--repoand then errors on the malformed repo argument — no browser opens. The defect is that the guard's safety currently depends on the downstream parser rejecting it, rather than on the guard's own classification. That is a latent correctness gap in an enforcement control: it holds today for a reason the guard does not itself assert, and it would stop holding if any value flag ever gained a permissive value grammar.Expected behaviour
A word consumed as a flag value should still be rejected when it is itself a denied flag — or the guard should assert explicitly why consuming it is safe for that specific flag.
Note the tension to resolve rather than paper over: some legitimate values genuinely begin with
-(a--searchexpression such as-label:foo, a--jqprogram). So this is not a blanket "reject dash-leading values" — it needs a deliberate rule, which is why it is filed rather than patched inline.Acceptance criteria
--search -label:x,--jqprograms) keep working — pinned by tests inforge-readonly-guard.test.shin both directions.Rough size: S–M — one classification site, but the value-grammar question needs deciding per flag family.
Provenance
Found while fixing the P1 on #177 (the guard denying the mandated bare
--jsonvocabulary probe). Reported rather than folded into that PR to keep it one concern; #177's fix neither introduces nor widens this.