issue #3389 · 2026-08-10

Our label taxonomy, enforced on a stranger's repo.

Filing a bug against a third-party project was refused outright because the command carried no --label. The label it wanted belongs to this repo's vocabulary, and the enforcer had no way to know it was pointing at someone else's. The milestone enforcer, in the same breath, suggested assigning the foreign issue to one of our sprints.

What actually ran

$ gh issue create --repo herdrdev/herdr --title "..." --body-file ...
BLOCKED: Missing --label flag. Add a label to categorize this issue:
  --label bug / enhancement / chore / docs
[gh-milestone-enforcer] No --milestone set. Consider assigning to a milestone.

Two independent defects

1. Scope — the hooks are repo-blind

Both are pure flag parsers with no network and no file I/O, which is what keeps them inside the 50ms PreToolUse budget. That same property means they cannot check whether the target repo has a bug label at all. Enforcing anyway ranges from noise (our sprint milestones) to hard breakage: gh exits non-zero when a label does not exist there, so "helpfully" appending one would turn a nit into a failed command.

2. Posture — deny was the wrong tool for a nit

A missing label is auto-correctable. A PreToolUse deny is not a nudge, it aborts before execution and costs the whole turn. CC 2.1.25 updatedInput exists precisely so a hook can fix the command instead of rejecting it.

The new decision table

commandlabel hookmilestone hook
gh issue create --title "Fix crash"rewrite: append --label bugadvisory
... --title "Update README typos"rewrite: --label docsadvisory
... --title "Weekly dep sweep"rewrite: --label choreadvisory
... --repo other/repo ...skip entirelyskip entirely
GH_REPO=other/repo gh issue createskip entirelyskip entirely
cat > f <<EOF ... EOF && gh issue createadvisory onlyadvisory

Inference is title-driven and ordered, so docs outranks bug and "fix typo in README" lands as docs rather than a bug report. Unmatched titles get chore, the one label every governed repo in this estate defines.

Two carve-outs kept deliberately

Compound commands still only advise (#3283)

Appending --label to A && B && C cannot target the right segment without a real shell parser, and denying a compound discards the segments that already matter, which is the original #3283 report. So compounds keep the advisory and nothing is rewritten.

The rewrite grants nothing

The result sets updatedInput and additionalContext but deliberately no permissionDecision. Returning 'allow' would skip the permission prompt as a side effect of a labelling nit, which is a strictly worse trade than the deny it replaced. A test pins permissionDecision as undefined.

Verification

gh-label + gh-milestone + sync-bash-dispatcher   86 passed, 0 failed
full hook unit suite    7559 passed, 2 skipped, 6 failed  <- all in context-gate, pre-existing
                        reproduced identically at origin/main in a 2nd worktree, filed as #3391
tsc --noEmit            clean

One thing checked before building the auto-correct, because the milestone this lands in exists to stop exactly this: sync-bash-dispatcher genuinely propagates a single hook's updatedInput (collected at :197, merged at :112-135). Had it dropped the field the way it once dropped ask-tier decisions, the rewrite would have been another no-op that reads as a working feature, which is the #3386 shape this repo is currently paying down.