cli-guard missing interpreter denies and absolute-path vault deny #19

Closed
opened 2026-05-27 02:46:49 +00:00 by coilysiren · 0 comments
Owner

Problem

Background security review on agentic-os flagged two gaps in the current lockdown surface, both verified against the live cli-guard code:

Gap 1: cli-guard does not block shell interpreters or process-spawning utilities.

cmd/agent-guard/hook.go coilyRoutes and agentGuardRoutes cover privileged-ops wrappers, package managers, build runners, and mcporter. They do not cover:

  • POSIX shells: bash, sh, zsh, dash, ksh, fish
  • Scripting runtimes: python, python3, ruby, perl, node, deno
  • Windows interpreters: cmd, powershell, pwsh, cscript, wscript, mshta, rundll32, regsvr32
  • macOS: osascript
  • Process spawning: exec, env, xargs (note: env is currently only handled as a prefix stripper in stripEnvPrefix, not denied)
  • Shell exfil patterns: echo*$*, printf*$* (variable expansion that leaks env to stdout)

These were previously enforced via .claude/settings.json deny entries. The recent migration to cli-guard (lockdown-deny.sh -> coily hook pre-tool-use) removed them from the JSON without adding equivalents to the Go routing tables. Net effect: bare bash -c 'cat ~/.aws/credentials' is no longer blocked.

Gap 2: vault read deny is tilde-form only.

cmd/coily/ops_lockdown.go applyDataSecurityDenies injects Read(~/projects/coilysiren/coilyco-vault/**) at data_security=high or stricter. The test at ops_lockdown_datasec_test.go:60 explicitly forbids the hardcoded /Users/kai/ form, presumably to avoid baking in a username.

Claude Code's permission matcher is a literal string compare, so Read(~/projects/...) only matches when the request canonicalizes through ~. An agent passing the absolute path Read(/Users/kai/projects/coilysiren/coilyco-vault/...) bypasses the deny.

Proposed fix

  1. Add an interpretersRoutes (or merge into coilyRoutes / agentGuardRoutes) table for bash/python/etc. with a deny-default and a recovery hint pointing to coily exec <verb> or named wrappers. Same shape as existing routing.
  2. Add deny patterns for exec:, env: (when not just a prefix), xargs:, and echo/printf with $.
  3. In applyDataSecurityDenies, emit BOTH forms of the vault deny. Compute the absolute path at runtime via os.UserHomeDir() so the literal /Users/kai/ is not baked in (satisfies the existing test's intent) but the absolute form is still present in the rendered settings.json.

Encountered in

Background security review during coilysiren/agentic-os work 2026-05-26. Both gaps confirmed by reading the routing tables and the lockdown injector directly.

Out of scope

The data_security tiers, the coily/agent-guard split, the Claude Code permission-matcher semantics. All three are upstream of this fix.

**Problem** Background security review on agentic-os flagged two gaps in the current lockdown surface, both verified against the live cli-guard code: **Gap 1: cli-guard does not block shell interpreters or process-spawning utilities.** `cmd/agent-guard/hook.go` `coilyRoutes` and `agentGuardRoutes` cover privileged-ops wrappers, package managers, build runners, and mcporter. They do not cover: - POSIX shells: bash, sh, zsh, dash, ksh, fish - Scripting runtimes: python, python3, ruby, perl, node, deno - Windows interpreters: cmd, powershell, pwsh, cscript, wscript, mshta, rundll32, regsvr32 - macOS: osascript - Process spawning: exec, env, xargs (note: `env` is currently only handled as a prefix stripper in `stripEnvPrefix`, not denied) - Shell exfil patterns: `echo*$*`, `printf*$*` (variable expansion that leaks env to stdout) These were previously enforced via `.claude/settings.json` `deny` entries. The recent migration to cli-guard (`lockdown-deny.sh` -> `coily hook pre-tool-use`) removed them from the JSON without adding equivalents to the Go routing tables. Net effect: bare `bash -c 'cat ~/.aws/credentials'` is no longer blocked. **Gap 2: vault read deny is tilde-form only.** `cmd/coily/ops_lockdown.go` `applyDataSecurityDenies` injects `Read(~/projects/coilysiren/coilyco-vault/**)` at `data_security=high` or stricter. The test at `ops_lockdown_datasec_test.go:60` explicitly forbids the hardcoded `/Users/kai/` form, presumably to avoid baking in a username. Claude Code's permission matcher is a literal string compare, so `Read(~/projects/...)` only matches when the request canonicalizes through `~`. An agent passing the absolute path `Read(/Users/kai/projects/coilysiren/coilyco-vault/...)` bypasses the deny. **Proposed fix** 1. Add an `interpretersRoutes` (or merge into `coilyRoutes` / `agentGuardRoutes`) table for bash/python/etc. with a deny-default and a recovery hint pointing to `coily exec <verb>` or named wrappers. Same shape as existing routing. 2. Add deny patterns for `exec:`, `env:` (when not just a prefix), `xargs:`, and `echo`/`printf` with `$`. 3. In `applyDataSecurityDenies`, emit BOTH forms of the vault deny. Compute the absolute path at runtime via `os.UserHomeDir()` so the literal `/Users/kai/` is not baked in (satisfies the existing test's intent) but the absolute form is still present in the rendered settings.json. **Encountered in** Background security review during coilysiren/agentic-os work 2026-05-26. Both gaps confirmed by reading the routing tables and the lockdown injector directly. **Out of scope** The data_security tiers, the coily/agent-guard split, the Claude Code permission-matcher semantics. All three are upstream of this fix.
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#19
No description provided.