Add hook.ForbiddenArgv: glob-pattern argv deny primitive #60

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

Scope

Add an argv-pattern deny primitive to hook.PreToolUse. Today's hook.Protected is basename-aware only — it can refuse gh entirely, but can't distinguish read verbs (gh pr view) from write verbs (gh pr create). This gap blocks two downstream consumer rules in kap:

  1. Block gh writes while leaving reads intact.
  2. Refuse any gh / forgejo invocation that mentions a forbidden substring (Kapwing-side example: stop agents filing Kapwing issues on GitHub when Kapwing tracks work in Shortcut).

Both rules fit a single shell-glob match against the segment argv string. Globs only — no regex.

Schema

Extends repocfg.Security:

security:
  forbidden_argv:
    - description: "gh writes"          # required, surfaced in the deny hint
      matches_glob_any:                 # POSIX fnmatch
        - "gh * create"
        - "gh * delete"
        - "gh * edit"
        - "gh * close"
        - "gh * reopen"
        - "gh * merge"
        - "gh * comment"
      hint: "use the issue tracker that owns this work, not gh."
    - description: "kapwing mentions in gh/fj API calls"
      matches_glob_any:
        - "gh *kapwing*"
        - "forgejo *kapwing*"
        - "fj *kapwing*"
      hint: "Kapwing work belongs in Shortcut, not gh/forgejo."

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

Engine

  • New hook.ForbiddenArgv type matching the YAML shape.
  • 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>}.
  • Matching uses path/filepath.Match against the post-StripEnvPrefix segment so env FOO=bar gh issue create classifies the same as bare gh issue create.

Acceptance

  • Schema parses round-trip via repocfg.Load; malformed entries (missing description, empty matches_glob_any, invalid glob syntax) return parse errors with file:line.
  • hook.PreToolUse denies a segment matching any glob; passes through segments matching none.
  • Allowed: gh pr view, gh issue list, gh api repos/foo/bar, gh api graphql -f query=foo (no kapwing mention).
  • Denied: gh issue create, gh pr create -t foo, gh repo delete, env GH_TOKEN=x gh release edit, gh api repos/kapwing/foo, forgejo issue create --repo kapwing/....
  • Tests cover: glob match per pattern, no-match pass-through, env-prefix strip, env-strip plus match, malformed schema rejection, hint surface in Decision.Message.
  • ward refactors to consume the new variadic (no behavior change yet; ward's own .ward/ward.yaml declares nothing under forbidden_argv).
  • docs/FEATURES.md entry updated; godoc covers the new type and the variadic.

Out of scope

  • Regex matching. Globs only by Kai's directive — keeps the schema legible and the runtime bounded.
  • Inverse "allowlist" framing. If a consumer wants "only these subcommands of gh allowed", express it via deny-everything-else later; not for this issue.
  • kap-side adoption. Tracked in a Shortcut story once this engine ships.

closes #N

## Scope Add an argv-pattern deny primitive to `hook.PreToolUse`. Today's `hook.Protected` is basename-aware only — it can refuse `gh` entirely, but can't distinguish read verbs (`gh pr view`) from write verbs (`gh pr create`). This gap blocks two downstream consumer rules in kap: 1. Block `gh` writes while leaving reads intact. 2. Refuse any `gh` / forgejo invocation that mentions a forbidden substring (Kapwing-side example: stop agents filing Kapwing issues on GitHub when Kapwing tracks work in Shortcut). Both rules fit a single shell-glob match against the segment argv string. Globs only — no regex. ## Schema Extends `repocfg.Security`: ```yaml security: forbidden_argv: - description: "gh writes" # required, surfaced in the deny hint matches_glob_any: # POSIX fnmatch - "gh * create" - "gh * delete" - "gh * edit" - "gh * close" - "gh * reopen" - "gh * merge" - "gh * comment" hint: "use the issue tracker that owns this work, not gh." - description: "kapwing mentions in gh/fj API calls" matches_glob_any: - "gh *kapwing*" - "forgejo *kapwing*" - "fj *kapwing*" hint: "Kapwing work belongs in Shortcut, not gh/forgejo." ``` Required: `description`, `matches_glob_any`. Optional: `hint` (synthesized "argv matched <glob>" deny used if absent). ## Engine - New `hook.ForbiddenArgv` type matching the YAML shape. - 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>}`. - Matching uses `path/filepath.Match` against the post-`StripEnvPrefix` segment so `env FOO=bar gh issue create` classifies the same as bare `gh issue create`. ## Acceptance - Schema parses round-trip via `repocfg.Load`; malformed entries (missing `description`, empty `matches_glob_any`, invalid glob syntax) return parse errors with file:line. - `hook.PreToolUse` denies a segment matching any glob; passes through segments matching none. - Allowed: `gh pr view`, `gh issue list`, `gh api repos/foo/bar`, `gh api graphql -f query=foo` (no kapwing mention). - Denied: `gh issue create`, `gh pr create -t foo`, `gh repo delete`, `env GH_TOKEN=x gh release edit`, `gh api repos/kapwing/foo`, `forgejo issue create --repo kapwing/...`. - Tests cover: glob match per pattern, no-match pass-through, env-prefix strip, env-strip plus match, malformed schema rejection, hint surface in Decision.Message. - ward refactors to consume the new variadic (no behavior change yet; ward's own `.ward/ward.yaml` declares nothing under `forbidden_argv`). - `docs/FEATURES.md` entry updated; godoc covers the new type and the variadic. ## Out of scope - Regex matching. Globs only by Kai's directive — keeps the schema legible and the runtime bounded. - Inverse "allowlist" framing. If a consumer wants "only these subcommands of gh allowed", express it via deny-everything-else later; not for this issue. - kap-side adoption. Tracked in a Shortcut story once this engine ships. closes #N
Author
Owner

Superseded by #61 - same scope, rewritten as a clean-room ticket scrubbed of in-session context and consumer-specific framing.

Superseded by #61 - same scope, rewritten as a clean-room ticket scrubbed of in-session context and consumer-specific framing.
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#60
No description provided.