2026-08-03 Ask Rule Precedence and the gh api Read Prompts
What I set out to do
Work out why gh api calls were throwing permission prompts, on the assumption that some read-only requests were getting caught by an over-broad filter in the permission config.
What I actually did
The assumption was right, and the prompt UI named the culprit itself: Ask rule Bash(gh api * -f *) overrides auto mode for this command. The rule exists for a real reason — gh api silently switches to POST when a field flag is present and no --method is given — but gh also has no way to pass query parameters on a GET other than a field flag, so every gh api -X GET search/issues -f q='...' read tripped the write guard. 129 quoted-URL calls and 8 search reads across the transcripts, and the reads were the common case.
First instinct was to allowlist around it. That doesn’t work, and the binary says why: XOd returns deny → ask → allow within the exact tier, and VZ_ evaluates a prefix-tier ask before returning the exact-tier allow. No allow rule, however specific, outranks an ask. The only thing that does is a PreToolUse hook returning permissionDecision: "allow", which yields a hookPermissionResult short-circuiting rule evaluation entirely. Permission globs have no negation, so “field flags except with an explicit GET” isn’t expressible either.
Settled on anchoring the field-flag asks to endpoints that accept writes (repos*, orgs*, user*, gists*, graphql*, markdown*, …, both slash spellings), generated in claude.nix rather than hand-listed. A read names its method before the path and escapes; an implicit POST names its path first and is still caught. Tests in nix/tests/claude-settings went red first, then green. Commit fd2684c.
The tail of the session was a self-inflicted detour. One command ran silently that should have asked, and I called it a pipe bypass and wrote it into the commit message as a known gap. It wasn’t: piped, redirected, quoted and bare forms all prompted on retest. The silent run had happened seconds after hm switch rewrote settings.json, mid-reload. Amended the commit and corrected the memory note.
Also mapped auto mode while in there: defaultMode: "auto" is honored only from policy/user/flag settings and silently downgraded from project or local settings (those are repo-controllable), there’s a cached circuit breaker that falls back to default, and the classifier runs last — only after rules yield nothing. Agent, ScheduleWakeup, CronCreate and RemoteTrigger opt out of auto-allow entirely.
What was striking
Two things, both about trusting the wrong evidence. The prompt dialog had been printing the exact offending rule the whole time — the diagnosis was sitting in the UI before any investigation started. And then I overrode a correct static reading of the decision function (N !== undefined && P === 1 returns the ask before the classifier is ever reached) on the strength of one anomalous observation, when the observation was the thing that was wrong. Same failure mode as tuning a parameter instead of falsifying the hypothesis, just pointed at my own reasoning: I had the answer and discarded it for a single data point taken in a window where the system was known to be in flux.
The narrower lesson worth keeping: a hm switch that rewrites settings.json leaves a gap where permission rules aren’t live yet, so nothing observed in that window means anything.
Related
- Commit
fd2684cin the dotfiles repo - 2026-08-01 Grep Translation Hook Removed After Finding Upstream Shims
- 2026-07-24 Claude Code Context Audit - Cutting Superpowers and 8 Plugins