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

Open
opened 2026-05-23 20:53:17 +00:00 by coilysiren · 0 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.
coilysiren added
P2
and removed
P1
labels 2026-05-31 07:01:27 +00:00
Sign in to join this conversation.
No labels
P0
P1
P2
P3
P4
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.