Add hook.ForbiddenArgv: glob-pattern argv deny primitive (clean-room) #61

Closed
opened 2026-06-04 00:32:12 +00:00 by coilysiren · 1 comment
Owner

Scope

Add an argv-pattern deny primitive to the PreToolUse hook engine.

Today's hook.Protected is basename-aware only - it can refuse gh entirely, but it cannot distinguish read verbs (gh pr view) from write verbs (gh pr create). It also cannot pattern-match on argv content (e.g. refuse any invocation that mentions a particular substring).

Both gaps fit a single shell-glob match against the segment argv string. POSIX fnmatch semantics, not regex.

Schema

Extends repocfg.Security:

security:
  forbidden_argv:
    - description: "gh writes"          # required, surfaced in the deny hint
      matches_glob_any:                 # POSIX fnmatch (path/filepath.Match)
        - "gh * create"
        - "gh * delete"
        - "gh * edit"
        - "gh * close"
        - "gh * reopen"
        - "gh * merge"
        - "gh * comment"
      hint: "use the relevant issue tracker, not gh."
    - description: "substring deny on gh / forgejo / fj"
      matches_glob_any:
        - "gh *<substring>*"
        - "forgejo *<substring>*"
        - "fj *<substring>*"
      hint: "this work belongs in the dedicated tracker, not gh/forgejo."

Required: description, matches_glob_any. Optional: hint (engine synthesizes "argv matched " deny if absent).

Patterns are matched via path/filepath.Match against the post-StripEnvPrefix segment so env FOO=bar gh issue create classifies the same as bare gh issue create.

Engine

  • New hook.ForbiddenArgv type matching the YAML shape (Description, MatchesGlobAny, Hint).
  • New variadic argument forbidden ...ForbiddenArgv on PreToolUse (peer to the existing protected ...Protected).
  • evaluateSegment runs the segment through every ForbiddenArgv.MatchesGlobAny before falling through to routes / integrity rules. First glob match wins; emits Decision{Block: true, Message: <hint>}.
  • Schema validation rejects: missing description, empty matches_glob_any, invalid glob syntax (anything path/filepath.Match returns ErrBadPattern for).

Acceptance

  • Schema parses round-trip via repocfg.Load; malformed entries return parse errors anchored to file:line.
  • hook.PreToolUse denies a segment matching any glob; passes through segments matching none.
  • Allowed examples: gh pr view, gh issue list, gh api repos/foo/bar, gh api graphql -f query=foo (no forbidden substring).
  • Denied examples: gh issue create, gh pr create -t foo, gh repo delete, env GH_TOKEN=x gh release edit, plus the substring rule firing on gh api repos/<substring>/foo and forgejo issue create --repo <substring>/....
  • Tests cover: glob match per pattern shape, no-match pass-through, env-prefix strip, env-strip plus match, malformed schema rejection, hint surface in Decision.Message, custom hint vs synthesized hint.
  • Existing protected ...Protected behavior unchanged. Consumers that pass only the existing variadics see identical output.
  • A consumer bridge (e.g. hookcfg.ForbiddenFor(repocfg.Security) paralleling the existing hookcfg.ProtectedFor) maps the parsed schema into the engine type.
  • docs/FEATURES.md mentions the new type; godoc covers the new variadic and bridge function.

Out of scope

  • Regex matching. Globs only - keeps the schema legible and the runtime bounded.
  • Inverse "allowlist" framing (only-these-subcommands-allowed). Express it as deny-everything-else if needed later; not for this issue.
  • Consumer-side adoption. Once this engine ships, individual consumers add their forbidden_argv blocks in their own follow-ups.

closes #N

## Scope Add an argv-pattern deny primitive to the PreToolUse hook engine. Today's `hook.Protected` is basename-aware only - it can refuse `gh` entirely, but it cannot distinguish read verbs (`gh pr view`) from write verbs (`gh pr create`). It also cannot pattern-match on argv content (e.g. refuse any invocation that mentions a particular substring). Both gaps fit a single shell-glob match against the segment argv string. POSIX `fnmatch` semantics, not regex. ## Schema Extends `repocfg.Security`: ```yaml security: forbidden_argv: - description: "gh writes" # required, surfaced in the deny hint matches_glob_any: # POSIX fnmatch (path/filepath.Match) - "gh * create" - "gh * delete" - "gh * edit" - "gh * close" - "gh * reopen" - "gh * merge" - "gh * comment" hint: "use the relevant issue tracker, not gh." - description: "substring deny on gh / forgejo / fj" matches_glob_any: - "gh *<substring>*" - "forgejo *<substring>*" - "fj *<substring>*" hint: "this work belongs in the dedicated tracker, not gh/forgejo." ``` Required: `description`, `matches_glob_any`. Optional: `hint` (engine synthesizes "argv matched <glob>" deny if absent). Patterns are matched via `path/filepath.Match` against the post-`StripEnvPrefix` segment so `env FOO=bar gh issue create` classifies the same as bare `gh issue create`. ## Engine - New `hook.ForbiddenArgv` type matching the YAML shape (Description, MatchesGlobAny, Hint). - New variadic argument `forbidden ...ForbiddenArgv` on `PreToolUse` (peer to the existing `protected ...Protected`). - `evaluateSegment` runs the segment through every `ForbiddenArgv.MatchesGlobAny` before falling through to routes / integrity rules. First glob match wins; emits `Decision{Block: true, Message: <hint>}`. - Schema validation rejects: missing `description`, empty `matches_glob_any`, invalid glob syntax (anything `path/filepath.Match` returns `ErrBadPattern` for). ## Acceptance - Schema parses round-trip via `repocfg.Load`; malformed entries return parse errors anchored to file:line. - `hook.PreToolUse` denies a segment matching any glob; passes through segments matching none. - Allowed examples: `gh pr view`, `gh issue list`, `gh api repos/foo/bar`, `gh api graphql -f query=foo` (no forbidden substring). - Denied examples: `gh issue create`, `gh pr create -t foo`, `gh repo delete`, `env GH_TOKEN=x gh release edit`, plus the substring rule firing on `gh api repos/<substring>/foo` and `forgejo issue create --repo <substring>/...`. - Tests cover: glob match per pattern shape, no-match pass-through, env-prefix strip, env-strip plus match, malformed schema rejection, hint surface in `Decision.Message`, custom hint vs synthesized hint. - Existing `protected ...Protected` behavior unchanged. Consumers that pass only the existing variadics see identical output. - A consumer bridge (e.g. `hookcfg.ForbiddenFor(repocfg.Security)` paralleling the existing `hookcfg.ProtectedFor`) maps the parsed schema into the engine type. - `docs/FEATURES.md` mentions the new type; godoc covers the new variadic and bridge function. ## Out of scope - Regex matching. Globs only - keeps the schema legible and the runtime bounded. - Inverse "allowlist" framing (only-these-subcommands-allowed). Express it as deny-everything-else if needed later; not for this issue. - Consumer-side adoption. Once this engine ships, individual consumers add their `forbidden_argv` blocks in their own follow-ups. closes #N
coilysiren added
P3
and removed
P2
labels 2026-06-17 08:40:02 +00:00
Author
Owner

🔒 Reserved by ward agent claude — container ward-cli-guard-issue-61-claude-fe295423 on host kais-macbook-pro.local is carrying this issue (reserved 2026-06-18T07:29:45Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent claude` — container `ward-cli-guard-issue-61-claude-fe295423` on host `kais-macbook-pro.local` is carrying this issue (reserved 2026-06-18T07:29:45Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/cli-guard#61
No description provided.