Add security doctor and downstream lockdown workflow for protected CLI isolation #4

Closed
opened 2026-05-23 20:53:17 +00:00 by coilysiren · 2 comments
Owner

Originally filed by @coilysiren on 2026-05-22T00:36:42Z - https://github.com/coilysiren/agent-guard/issues/28

Context

agent-guard is the shipped binary for repo-facing agent safety workflows: it is Homebrew-distributed, owns the executable contract, and already consumes cli-guard primitives. cli-guard remains the reusable Go framework/library. Downstream repos should be able to adopt this with config plus thin shell wrappers, without carrying Go code or checked-in binaries.

The operator goal is to make it difficult for Codex/Claude/other semi-trusted agents to run privileged or sensitive host tools directly, while still giving humans a clean path through audited wrappers.

This should be implementable clean-room from this issue plus the existing repo. Do not invent a new config scheme: downstream repos already use .agent-guard/agent-guard.yaml, parsed through cli-guard/repocfg.

Current state

In this repo today:

  • .agent-guard/agent-guard.yaml declares allowed repo commands.
  • cmd/agent-guard/repocfg.go discovers config by walking up from cwd:
    • .agent-guard/agent-guard.yaml
    • .coily/coily.yaml
  • agent-guard exec <verb> runs configured repo commands.
  • agent-guard lint validates the config/Makefile contract.
  • agent-guard hook pre-tool-use exists for Claude Code Bash PreToolUse payloads.
  • agent-guard install-hooks registers that hook.
  • cli-guard/hook already provides segment splitting, route hints, binary path integrity rules, interpreter denies, and scratch-dir exec denies.
  • cli-guard/lockdown/defaults.yaml demonstrates the intended inversion: deny bare privileged binaries and route privileged operations through a wrapper binary.

Desired architecture

Use this split:

  • cli-guard

    • reusable security primitives only
    • hook parser / decision engine
    • doctor/check primitives if they are generic
    • lockdown helpers if generic enough
    • no downstream-specific policy baked into library packages
  • agent-guard

    • the compiled, Homebrew-installed binary
    • owns the executable command surface
    • reads downstream config
    • exposes security doctor / verify / hook / install commands
    • can embed generic defaults
    • exposes generic policy knobs consumed by downstream wrappers
  • downstream repo/tooling

    • shell UX and config only
    • sets an env var pointing agent-guard at the repo config path when needed
    • delegates to installed agent-guard
    • no Go code required in the downstream repo
    • no checked-in compiled binary required for v1

Add explicit config path support

Add an env-var override so downstream shell wrappers do not need to run from the config repo.

Suggested name:

AGENT_GUARD_CONFIG=/absolute/path/to/.agent-guard/agent-guard.yaml

Behavior:

  1. If a --config <path> flag is present on commands that load repo config, use it.
  2. Else if AGENT_GUARD_CONFIG is set, use it.
  3. Else fall back to current cwd walk-up discovery.

Apply this consistently to commands that need config:

  • agent-guard exec
  • agent-guard lint
  • new doctor/verify commands from this issue
  • hook behavior if/when it needs downstream policy

Validation:

  • path must exist
  • path must not be a directory
  • parse using existing repocfg.Load
  • error messages should name which source was selected: --config, AGENT_GUARD_CONFIG, or discovery

Add security policy fields to the existing config

Do not replace the current commands: scheme. Extend the existing YAML with an optional security section.

Example shape:

commands:
  build:
    run: make build
    description: Build all packages.

security:
  protectedBinaries:
    - name: example-cloud
      mode: deny-direct
      allowedWrappers:
        - repo-tool
        - agent-guard
      expectedRealPaths:
        - /opt/homebrew/bin/example-cloud
        - /usr/local/bin/example-cloud
      credentialEnv:
        - EXAMPLE_CLOUD_CONFIG
        - EXAMPLE_APPLICATION_CREDENTIALS

    - name: example-clusterctl
      mode: deny-direct
      allowedWrappers:
        - repo-tool
        - agent-guard
      expectedRealPaths:
        - /opt/homebrew/bin/example-clusterctl
        - /usr/local/bin/example-clusterctl
      credentialEnv:
        - EXAMPLE_CLUSTER_CONFIG

  sudo:
    forbidPasswordless: true

  hooks:
    denyBareBinaries:
      - example-cloud
      - example-clusterctl
    routeHints:
      example-cloud: "Use the approved repo wrapper for cloud operations."
      example-clusterctl: "Use the approved repo wrapper for cluster operations."

The exact field names can change, but preserve the existing config-file pattern and keep this declarative.

Add agent-guard doctor security

Add a new doctor command focused on host readiness and agent lockout posture.

Suggested command:

agent-guard doctor security
agent-guard doctor security --config /path/to/.agent-guard/agent-guard.yaml
agent-guard doctor security --json

Checks:

  1. Config loads successfully from --config, AGENT_GUARD_CONFIG, or discovery.
  2. Current agent-guard binary resolves to a canonical trusted install path.
  3. Protected binaries are not directly reachable in a way that bypasses wrappers.
  4. PATH shim posture is sane:
    • a protected binary found early on PATH should be a deny shim or approved wrapper shim
    • the real binary should not be casually invokable by the agent user through normal PATH lookup
    • report absolute-path bypass risk separately if the real binary remains user-executable
  5. Passwordless sudo posture:
    • detect whether the current user can run broad sudo without a password
    • fail/warn when sudo -n true succeeds and policy forbids passwordless sudo
    • ideally distinguish broad passwordless sudo from a narrow allowlist if feasible without parsing every sudoers edge case
  6. Credential posture:
    • warn/fail if configured credential env vars are set in the current session
    • warn/fail if configured default credential files exist in locations agents commonly inherit, where detection is cheap and generic enough
  7. Hook/permission posture:
    • report whether Claude hook is installed when relevant
    • report whether Codex hook/config integration is installed when implemented
    • report whether deny rules for bare protected binaries are present where the target tool supports static deny lists

Output:

  • Human output should be direct and hard to miss.
  • JSON output should be stable enough for wrapper CLIs and CI.
  • Use explicit severities: pass, warn, fail.
  • Include remediation text per finding.

Important security stance:

  • Hooks are a guardrail, not the whole boundary.
  • A serious lockout requires no broad passwordless sudo and no habit of entering sudo passwords into agent sessions.
  • Do not claim perfect enforcement when absolute-path invocation of a real user-executable binary remains possible.

Add agent-guard verify security or make doctor CI-friendly

There should be a non-interactive mode suitable for pre-commit/CI:

agent-guard doctor security --check

or:

agent-guard verify security

Expected behavior:

  • exits non-zero on fail
  • optionally exits non-zero on warn with --strict
  • no prompts
  • JSON mode available

This lets downstream repos add a pre-commit or CI check that validates the security config without committing generated binaries.

Hook behavior

For v1, deny configured protected binaries wholesale when invoked directly. Route users toward configured wrapper commands.

Consider expanding current hook matching so route and deny checks handle these spellings consistently:

example-clusterctl get pods
example-cloud projects list
/opt/homebrew/bin/example-clusterctl get pods
/usr/local/bin/example-cloud auth list
env EXAMPLE_CLUSTER_CONFIG=x example-clusterctl get pods
sudo example-clusterctl get pods

The existing cli-guard/hook already strips env and sudo, and it already detects interpreter basenames by path. If route matching currently keys only on the full leading token, add basename-aware matching for protected binaries so absolute paths do not bypass hook hints/denies.

Codex note:

  • Claude Code PreToolUse is already wired.
  • Codex hook/tool paths may differ, especially around newer/streaming shell execution.
  • Implement Codex support where practical, but keep docs honest that shims/permissions/credential isolation are the stronger layer.

Downstream integration target

A downstream wrapper should be able to look like this:

export AGENT_GUARD_CONFIG="$REPO_ROOT/.agent-guard/agent-guard.yaml"
repo-tool doctor security     # delegates to agent-guard doctor security
repo-tool doctor              # includes security doctor summary

Contributors should not need to see or edit Go code in the downstream repo.

No downstream binary check-in is required for v1. Prefer Homebrew-installed agent-guard plus version/checksum reporting. If a future issue chooses checked-in binaries, require reproducible build verification and checksum/signature metadata.

Pre-commit recommendation

Do not have ordinary downstream pre-commit hooks compile and check in a binary.

Instead:

  • agent-guard repo owns build/test/release checks.
  • downstream repos run config validation:
agent-guard lint --config "$AGENT_GUARD_CONFIG"
agent-guard doctor security --config "$AGENT_GUARD_CONFIG" --check

If the config changes, the check should prove the installed/pinned agent-guard understands it.

Suggested implementation slices

  1. Add config-path resolution helper

    • supports --config
    • supports AGENT_GUARD_CONFIG
    • preserves current discovery fallback
    • add tests
  2. Extend config parsing

    • add optional security: section
    • keep existing commands: behavior unchanged
    • add tests for old configs and new configs
  3. Add doctor command tree

    • agent-guard doctor security
    • human output
    • --json
    • --check
    • tests around exit codes and rendered findings
  4. Implement host checks

    • binary path / PATH shim checks
    • passwordless sudo probe
    • credential env detection
    • protected binary reachability
  5. Wire hook policy from config

    • deny/hint bare protected binaries
    • basename-aware matching for absolute paths
    • preserve existing arbitrary-code denies
  6. Docs

    • README usage
    • docs/FEATURES.md
    • example config section
    • Homebrew install + downstream wrapper integration notes

Acceptance criteria

  • Existing agent-guard exec, lint, hook, and install-hooks behavior keeps working with the current .agent-guard/agent-guard.yaml.
  • AGENT_GUARD_CONFIG=/abs/path/file.yaml agent-guard lint works outside the repo tree.
  • agent-guard doctor security --config /abs/path/file.yaml --check exits non-zero when protected binaries or sudo posture violate policy.
  • agent-guard doctor security --json emits machine-readable findings with stable severity fields.
  • A config can declare protected binaries as direct-deny binaries.
  • Hook checks deny or route-hint bare and absolute-path invocations of configured protected binaries.
  • Docs explicitly state that hooks are not a complete security boundary and that serious lockout also requires shim/permission/credential/sudo posture.
  • No downstream-specific policy is hardcoded into generic cli-guard packages.
  • No compiled binary needs to be checked into a downstream repo for the v1 workflow.

Non-goals

  • Do not build a full sudoers parser unless a narrow, reliable implementation falls out naturally.
  • Do not claim OS-enforced parent-process authorization; Unix permissions do not provide that directly for same-user invocations.
  • Do not make any one organization's cloud allowlist the default for every agent-guard user.
  • Do not require custom downstream-flavored Go builds for v1.
  • Do not solve read-only protected-tool allowlists in v1; default-deny direct invocation is enough. A v2 can add audited read-only wrapper allowlists.
_Originally filed by @coilysiren on 2026-05-22T00:36:42Z - [https://github.com/coilysiren/agent-guard/issues/28](https://github.com/coilysiren/agent-guard/issues/28)_ ## Context `agent-guard` is the shipped binary for repo-facing agent safety workflows: it is Homebrew-distributed, owns the executable contract, and already consumes `cli-guard` primitives. `cli-guard` remains the reusable Go framework/library. Downstream repos should be able to adopt this with config plus thin shell wrappers, without carrying Go code or checked-in binaries. The operator goal is to make it difficult for Codex/Claude/other semi-trusted agents to run privileged or sensitive host tools directly, while still giving humans a clean path through audited wrappers. This should be implementable clean-room from this issue plus the existing repo. Do not invent a new config scheme: downstream repos already use `.agent-guard/agent-guard.yaml`, parsed through `cli-guard/repocfg`. ## Current state In this repo today: - `.agent-guard/agent-guard.yaml` declares allowed repo commands. - `cmd/agent-guard/repocfg.go` discovers config by walking up from cwd: - `.agent-guard/agent-guard.yaml` - `.coily/coily.yaml` - `agent-guard exec <verb>` runs configured repo commands. - `agent-guard lint` validates the config/Makefile contract. - `agent-guard hook pre-tool-use` exists for Claude Code `Bash` PreToolUse payloads. - `agent-guard install-hooks` registers that hook. - `cli-guard/hook` already provides segment splitting, route hints, binary path integrity rules, interpreter denies, and scratch-dir exec denies. - `cli-guard/lockdown/defaults.yaml` demonstrates the intended inversion: deny bare privileged binaries and route privileged operations through a wrapper binary. ## Desired architecture Use this split: - `cli-guard` - reusable security primitives only - hook parser / decision engine - doctor/check primitives if they are generic - lockdown helpers if generic enough - no downstream-specific policy baked into library packages - `agent-guard` - the compiled, Homebrew-installed binary - owns the executable command surface - reads downstream config - exposes security doctor / verify / hook / install commands - can embed generic defaults - exposes generic policy knobs consumed by downstream wrappers - downstream repo/tooling - shell UX and config only - sets an env var pointing `agent-guard` at the repo config path when needed - delegates to installed `agent-guard` - no Go code required in the downstream repo - no checked-in compiled binary required for v1 ## Add explicit config path support Add an env-var override so downstream shell wrappers do not need to run from the config repo. Suggested name: ```sh AGENT_GUARD_CONFIG=/absolute/path/to/.agent-guard/agent-guard.yaml ``` Behavior: 1. If a `--config <path>` flag is present on commands that load repo config, use it. 2. Else if `AGENT_GUARD_CONFIG` is set, use it. 3. Else fall back to current cwd walk-up discovery. Apply this consistently to commands that need config: - `agent-guard exec` - `agent-guard lint` - new doctor/verify commands from this issue - hook behavior if/when it needs downstream policy Validation: - path must exist - path must not be a directory - parse using existing `repocfg.Load` - error messages should name which source was selected: `--config`, `AGENT_GUARD_CONFIG`, or discovery ## Add security policy fields to the existing config Do not replace the current `commands:` scheme. Extend the existing YAML with an optional security section. Example shape: ```yaml commands: build: run: make build description: Build all packages. security: protectedBinaries: - name: example-cloud mode: deny-direct allowedWrappers: - repo-tool - agent-guard expectedRealPaths: - /opt/homebrew/bin/example-cloud - /usr/local/bin/example-cloud credentialEnv: - EXAMPLE_CLOUD_CONFIG - EXAMPLE_APPLICATION_CREDENTIALS - name: example-clusterctl mode: deny-direct allowedWrappers: - repo-tool - agent-guard expectedRealPaths: - /opt/homebrew/bin/example-clusterctl - /usr/local/bin/example-clusterctl credentialEnv: - EXAMPLE_CLUSTER_CONFIG sudo: forbidPasswordless: true hooks: denyBareBinaries: - example-cloud - example-clusterctl routeHints: example-cloud: "Use the approved repo wrapper for cloud operations." example-clusterctl: "Use the approved repo wrapper for cluster operations." ``` The exact field names can change, but preserve the existing config-file pattern and keep this declarative. ## Add `agent-guard doctor security` Add a new doctor command focused on host readiness and agent lockout posture. Suggested command: ```sh agent-guard doctor security agent-guard doctor security --config /path/to/.agent-guard/agent-guard.yaml agent-guard doctor security --json ``` Checks: 1. Config loads successfully from `--config`, `AGENT_GUARD_CONFIG`, or discovery. 2. Current `agent-guard` binary resolves to a canonical trusted install path. 3. Protected binaries are not directly reachable in a way that bypasses wrappers. 4. PATH shim posture is sane: - a protected binary found early on PATH should be a deny shim or approved wrapper shim - the real binary should not be casually invokable by the agent user through normal PATH lookup - report absolute-path bypass risk separately if the real binary remains user-executable 5. Passwordless sudo posture: - detect whether the current user can run broad sudo without a password - fail/warn when `sudo -n true` succeeds and policy forbids passwordless sudo - ideally distinguish broad passwordless sudo from a narrow allowlist if feasible without parsing every sudoers edge case 6. Credential posture: - warn/fail if configured credential env vars are set in the current session - warn/fail if configured default credential files exist in locations agents commonly inherit, where detection is cheap and generic enough 7. Hook/permission posture: - report whether Claude hook is installed when relevant - report whether Codex hook/config integration is installed when implemented - report whether deny rules for bare protected binaries are present where the target tool supports static deny lists Output: - Human output should be direct and hard to miss. - JSON output should be stable enough for wrapper CLIs and CI. - Use explicit severities: `pass`, `warn`, `fail`. - Include remediation text per finding. Important security stance: - Hooks are a guardrail, not the whole boundary. - A serious lockout requires no broad passwordless sudo and no habit of entering sudo passwords into agent sessions. - Do not claim perfect enforcement when absolute-path invocation of a real user-executable binary remains possible. ## Add `agent-guard verify security` or make doctor CI-friendly There should be a non-interactive mode suitable for pre-commit/CI: ```sh agent-guard doctor security --check ``` or: ```sh agent-guard verify security ``` Expected behavior: - exits non-zero on `fail` - optionally exits non-zero on `warn` with `--strict` - no prompts - JSON mode available This lets downstream repos add a pre-commit or CI check that validates the security config without committing generated binaries. ## Hook behavior For v1, deny configured protected binaries wholesale when invoked directly. Route users toward configured wrapper commands. Consider expanding current hook matching so route and deny checks handle these spellings consistently: ```sh example-clusterctl get pods example-cloud projects list /opt/homebrew/bin/example-clusterctl get pods /usr/local/bin/example-cloud auth list env EXAMPLE_CLUSTER_CONFIG=x example-clusterctl get pods sudo example-clusterctl get pods ``` The existing `cli-guard/hook` already strips `env` and `sudo`, and it already detects interpreter basenames by path. If route matching currently keys only on the full leading token, add basename-aware matching for protected binaries so absolute paths do not bypass hook hints/denies. Codex note: - Claude Code `PreToolUse` is already wired. - Codex hook/tool paths may differ, especially around newer/streaming shell execution. - Implement Codex support where practical, but keep docs honest that shims/permissions/credential isolation are the stronger layer. ## Downstream integration target A downstream wrapper should be able to look like this: ```sh export AGENT_GUARD_CONFIG="$REPO_ROOT/.agent-guard/agent-guard.yaml" repo-tool doctor security # delegates to agent-guard doctor security repo-tool doctor # includes security doctor summary ``` Contributors should not need to see or edit Go code in the downstream repo. No downstream binary check-in is required for v1. Prefer Homebrew-installed `agent-guard` plus version/checksum reporting. If a future issue chooses checked-in binaries, require reproducible build verification and checksum/signature metadata. ## Pre-commit recommendation Do not have ordinary downstream pre-commit hooks compile and check in a binary. Instead: - `agent-guard` repo owns build/test/release checks. - downstream repos run config validation: ```sh agent-guard lint --config "$AGENT_GUARD_CONFIG" agent-guard doctor security --config "$AGENT_GUARD_CONFIG" --check ``` If the config changes, the check should prove the installed/pinned `agent-guard` understands it. ## Suggested implementation slices 1. Add config-path resolution helper - supports `--config` - supports `AGENT_GUARD_CONFIG` - preserves current discovery fallback - add tests 2. Extend config parsing - add optional `security:` section - keep existing `commands:` behavior unchanged - add tests for old configs and new configs 3. Add doctor command tree - `agent-guard doctor security` - human output - `--json` - `--check` - tests around exit codes and rendered findings 4. Implement host checks - binary path / PATH shim checks - passwordless sudo probe - credential env detection - protected binary reachability 5. Wire hook policy from config - deny/hint bare protected binaries - basename-aware matching for absolute paths - preserve existing arbitrary-code denies 6. Docs - README usage - `docs/FEATURES.md` - example config section - Homebrew install + downstream wrapper integration notes ## Acceptance criteria - Existing `agent-guard exec`, `lint`, `hook`, and `install-hooks` behavior keeps working with the current `.agent-guard/agent-guard.yaml`. - `AGENT_GUARD_CONFIG=/abs/path/file.yaml agent-guard lint` works outside the repo tree. - `agent-guard doctor security --config /abs/path/file.yaml --check` exits non-zero when protected binaries or sudo posture violate policy. - `agent-guard doctor security --json` emits machine-readable findings with stable severity fields. - A config can declare protected binaries as direct-deny binaries. - Hook checks deny or route-hint bare and absolute-path invocations of configured protected binaries. - Docs explicitly state that hooks are not a complete security boundary and that serious lockout also requires shim/permission/credential/sudo posture. - No downstream-specific policy is hardcoded into generic `cli-guard` packages. - No compiled binary needs to be checked into a downstream repo for the v1 workflow. ## Non-goals - Do not build a full sudoers parser unless a narrow, reliable implementation falls out naturally. - Do not claim OS-enforced parent-process authorization; Unix permissions do not provide that directly for same-user invocations. - Do not make any one organization's cloud allowlist the default for every `agent-guard` user. - Do not require custom downstream-flavored Go builds for v1. - Do not solve read-only protected-tool allowlists in v1; default-deny direct invocation is enough. A v2 can add audited read-only wrapper allowlists.
Author
Owner

Progress + handoff (2026-06-03)

Building this issue's prerequisites bottom-up. Status:

Landed — cli-guard PR #56 (merged to main, tip e102d72)

The two generic framework primitives this issue's doctor/hook surfaces consume:

  • repocfg security: schema (closes cli-guard #54) — declarative block: protected_binaries (name, mode, allowed_wrappers, expected_real_paths, credential_env), sudo.forbid_passwordless, hooks.{deny_bare_binaries,route_hints}. Snake_case to match the commands: scheme. Parsed + validated (basename-only names, deny-direct mode, no dupes), enforces nothing. repocfg.Config.Security is zero when absent; old configs unchanged.
  • hook.Protected basename-aware deny (closes cli-guard #55) — denies a protected binary on the bare token, any absolute-path spelling (/opt/homebrew/bin/gcloud), after env/sudo strip, and piped behind an allowed command. Closes the absolute-path bypass the 2026-05-08 deny-uv finding documented. Trailing variadic on PreToolUse; exports Basename.

In review — ward PR #37 (closes ward #36): the de-fork

ward was pinning the old github.com/coilysiren/cli-guard path and had forked the hook engine, so the new primitives couldn't reach it. PR #37:

  1. Migrated go.mod to forgejo.coilysiren.me/coilyco-flight-deck/cli-guard@e102d72.
  2. Deleted ward's forked hook helpers; delegated the Bash decision to hook.PreToolUse (routes → []hook.Route with gh-GraphQL suffix via Route.Extra, guard binaries → []hook.IntegrityRule). ward keeps the file-write sidequest check + coily-vs-ward guard selection.

Gained the interpreter/exfil/scratch-exec denies the fork lacked (see #19). All existing hook tests pass; two new tests lock the gained denies.

Remaining for THIS issue (gated on #37 merging)

  1. Config-path resolution--config flag, then WARD_CONFIG env, else cwd walk-up; applied to exec/lint/hook/doctor; errors name the source. (AGENT_GUARD_CONFIG in the issue body = WARD_CONFIG; this issue predates the agent-guardward rename, so .agent-guard/ = .ward/.)
  2. Hook wiring — load cfg.Security.ProtectedBinaries in the ward hook and pass as hook.Protected so configured binaries (gcloud, clusterctl) are denied. The engine primitive already exists; this is the consumer wiring.
  3. ward doctor security [--json|--check|--strict] — consuming generic cli-guard doctor primitives still to be built in the framework: PATH-shim posture, protected-binary reachability, credential-env detection, sudo posture (reuse cli-guard sudo pkg). Severity pass/warn/fail + remediation. CI mode exits non-zero on fail.
  4. Docs — honest stance: hooks are a guardrail, not the whole boundary; serious lockout needs shim/permission/credential/sudo posture too.

Notes for whoever picks this up

  • Forgejo write on Kai's host: git push authenticates via git-credential-forgejo-ssm.sh; issues/PRs via coily ops forgejo issue|pr ....
  • cli-guard consumers set GOPRIVATE=forgejo.coilysiren.me to resolve the module.
  • Bigger plan: this is Phase 1 ("the logic work needs"). Phase 2 = convert coilyco-flight-deck/agentic-os to 100% ward / 0% coily (per-tree shed decisions on warp/gaming/ops). Phase 3 = the work deploy adopts cli-guard as a silent addition to its deploy git-clone, no Go in the work deploy.
## Progress + handoff (2026-06-03) Building this issue's prerequisites bottom-up. Status: ### Landed — cli-guard PR #56 (merged to `main`, tip `e102d72`) The two generic framework primitives this issue's `doctor`/hook surfaces consume: - **`repocfg` `security:` schema** (closes cli-guard #54) — declarative block: `protected_binaries` (`name`, `mode`, `allowed_wrappers`, `expected_real_paths`, `credential_env`), `sudo.forbid_passwordless`, `hooks.{deny_bare_binaries,route_hints}`. Snake_case to match the `commands:` scheme. Parsed + validated (basename-only names, `deny-direct` mode, no dupes), enforces nothing. `repocfg.Config.Security` is zero when absent; old configs unchanged. - **`hook.Protected` basename-aware deny** (closes cli-guard #55) — denies a protected binary on the bare token, **any absolute-path spelling** (`/opt/homebrew/bin/gcloud`), after `env`/`sudo` strip, and piped behind an allowed command. Closes the absolute-path bypass the 2026-05-08 deny-uv finding documented. Trailing variadic on `PreToolUse`; exports `Basename`. ### In review — ward PR #37 (closes ward #36): the de-fork ward was pinning the old `github.com/coilysiren/cli-guard` path and had **forked** the hook engine, so the new primitives couldn't reach it. PR #37: 1. Migrated go.mod to `forgejo.coilysiren.me/coilyco-flight-deck/cli-guard@e102d72`. 2. Deleted ward's forked hook helpers; delegated the Bash decision to `hook.PreToolUse` (routes → `[]hook.Route` with gh-GraphQL suffix via `Route.Extra`, guard binaries → `[]hook.IntegrityRule`). ward keeps the file-write sidequest check + coily-vs-ward guard selection. Gained the interpreter/exfil/scratch-exec denies the fork lacked (see #19). All existing hook tests pass; two new tests lock the gained denies. ### Remaining for THIS issue (gated on #37 merging) 1. **Config-path resolution** — `--config` flag, then `WARD_CONFIG` env, else cwd walk-up; applied to `exec`/`lint`/`hook`/`doctor`; errors name the source. (`AGENT_GUARD_CONFIG` in the issue body = `WARD_CONFIG`; this issue predates the `agent-guard` → `ward` rename, so `.agent-guard/` = `.ward/`.) 2. **Hook wiring** — load `cfg.Security.ProtectedBinaries` in the ward hook and pass as `hook.Protected` so configured binaries (gcloud, clusterctl) are denied. The engine primitive already exists; this is the consumer wiring. 3. **`ward doctor security [--json|--check|--strict]`** — consuming generic cli-guard doctor primitives still to be built in the framework: PATH-shim posture, protected-binary reachability, credential-env detection, sudo posture (reuse cli-guard `sudo` pkg). Severity `pass`/`warn`/`fail` + remediation. CI mode exits non-zero on fail. 4. **Docs** — honest stance: hooks are a guardrail, not the whole boundary; serious lockout needs shim/permission/credential/sudo posture too. ### Notes for whoever picks this up - Forgejo write on Kai's host: `git push` authenticates via `git-credential-forgejo-ssm.sh`; issues/PRs via `coily ops forgejo issue|pr ...`. - cli-guard consumers set `GOPRIVATE=forgejo.coilysiren.me` to resolve the module. - Bigger plan: this is Phase 1 ("the logic work needs"). Phase 2 = convert `coilyco-flight-deck/agentic-os` to 100% ward / 0% coily (per-tree shed decisions on warp/gaming/ops). Phase 3 = the work deploy adopts cli-guard as a silent addition to its deploy git-clone, no Go in the work deploy.
Author
Owner

All five original deliverables shipped across four sub-issues:

  • Config-path resolution--config flag and $WARD_CONFIG env, precedence --config > $WARD_CONFIG > cwd walk-up. Pre-parser runs before cli.Command construction so exec's init-time subtree honors the override. Closed by #38 (1109476).
  • Security policy schema — cli-guard PR #56 (repocfg.Security, basename-aware hook.Protected). Pre-merged before this epic landed.
  • ward doctor command — single diagnostic umbrella. ward doctor allowlist (current lint logic, resolver-aware), ward doctor security (config summary + host probes). ward lint kept as deprecation alias for one minor release so the cross-repo pre-commit suite keeps passing — removal tracked in #41. Closed by #40 (2448d4b).
  • CI-friendly modeward doctor security exits non-zero on any FAIL row. --skip <name> (repeatable) and --strict-credentials tune the gate per CI step. Baked into #42.
  • Hook integrationcfg.Security.ProtectedBinaries and Hooks.DenyBareBinaries feed cli-guard/hook.PreToolUse as Protected entries. Basename-aware match covers bare tokens, absolute paths, and relative-with-slash spellings. Best-effort load from Claude-supplied cwd; malformed config passes through silently. Closed by #43 (5631d12).

Host probes (closed by #42, 4a543ae):

  • pathexec.LookPath each protected binary, compare to expected_real_paths. PASS / FAIL / WARN / INFO.
  • sudosudo -n true when forbid_passwordless is set; cli-guard's sudo.PasswordRequired distinguishes "password needed" from "ran clean".
  • credentials — walk credential_env per binary, WARN on hits (FAIL under --strict-credentials).

Each probe is a pure function with dep-injected lookup / runner / getenv so unit tests drive deterministic results without touching the host.

Architecture line held. cli-guard owns reusable primitives (repocfg.Security, hook.Protected, sudo.PasswordRequired); ward owns the command surface, config-load orchestration, and host-probe wiring; downstream repos contribute only .ward/ward.yaml plus their wrappers. No Go code required in consumer repos.

Follow-ups still open:

  • #41 — remove the ward lint deprecation alias once the agentic-os pre-commit suite migrates to ward doctor allowlist.

Closing.

All five original deliverables shipped across four sub-issues: - **Config-path resolution** — `--config` flag and `$WARD_CONFIG` env, precedence `--config > $WARD_CONFIG > cwd walk-up`. Pre-parser runs before `cli.Command` construction so `exec`'s init-time subtree honors the override. Closed by #38 (`1109476`). - **Security policy schema** — cli-guard PR #56 (`repocfg.Security`, basename-aware `hook.Protected`). Pre-merged before this epic landed. - **`ward doctor` command** — single diagnostic umbrella. `ward doctor allowlist` (current lint logic, resolver-aware), `ward doctor security` (config summary + host probes). `ward lint` kept as deprecation alias for one minor release so the cross-repo pre-commit suite keeps passing — removal tracked in #41. Closed by #40 (`2448d4b`). - **CI-friendly mode** — `ward doctor security` exits non-zero on any FAIL row. `--skip <name>` (repeatable) and `--strict-credentials` tune the gate per CI step. Baked into #42. - **Hook integration** — `cfg.Security.ProtectedBinaries` and `Hooks.DenyBareBinaries` feed `cli-guard/hook.PreToolUse` as `Protected` entries. Basename-aware match covers bare tokens, absolute paths, and relative-with-slash spellings. Best-effort load from Claude-supplied `cwd`; malformed config passes through silently. Closed by #43 (`5631d12`). **Host probes** (closed by #42, `4a543ae`): - `path` — `exec.LookPath` each protected binary, compare to `expected_real_paths`. PASS / FAIL / WARN / INFO. - `sudo` — `sudo -n true` when `forbid_passwordless` is set; cli-guard's `sudo.PasswordRequired` distinguishes "password needed" from "ran clean". - `credentials` — walk `credential_env` per binary, WARN on hits (FAIL under `--strict-credentials`). Each probe is a pure function with dep-injected lookup / runner / getenv so unit tests drive deterministic results without touching the host. **Architecture line held.** cli-guard owns reusable primitives (`repocfg.Security`, `hook.Protected`, `sudo.PasswordRequired`); ward owns the command surface, config-load orchestration, and host-probe wiring; downstream repos contribute only `.ward/ward.yaml` plus their wrappers. No Go code required in consumer repos. **Follow-ups still open:** - #41 — remove the `ward lint` deprecation alias once the agentic-os pre-commit suite migrates to `ward doctor allowlist`. Closing.
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/ward#4
No description provided.