Skip to content

Writes to .env are unguarded, so the "operator-only" boundary several plugins rely on is not enforced #682

Description

@gtapps

Summary

Four domain plugins deny Edit(.env) in their settings.json and document .env as the operator-only surface. Nothing enforces that against a Bash write or against the Write tool, so an agent can add or change .env entries — including entries that widen its own permissions.

This is a real gap rather than a theoretical one now, because laravel-forge-hermit puts a security decision in .env: FORGE_POLICY_ALLOW_TIERS and FORGE_POLICY_ALLOW are what lift its secrets and destructive deny tiers, and its refusal text tells the operator "operator-only; the agent cannot edit .env". That sentence is currently not true.

Reproduction

Feeding payloads straight into the hook (AGENT_HOOK_PROFILE as noted, exit 2 = blocked):

tool call profile exit expected
echo FOO=1 >> .env standard 0 2
echo FOO=1 | tee -a .env standard 0 2
Write(file_path=".env") standard 0 2
echo x > .claude/settings.json strict 2 2
echo x>.claude/settings.json strict 0 2
rm -rf build standard 2 2 (control)

Three distinct problems

1. No deny-pattern covers writes to .env. state-templates/deny-patterns.json blocks reads (Bash(cat .env*), Bash(cat */.env*)) but no write form. The precedent for exactly this shape already exists for other protected files — Bash(*> *.claude-code-hermit/OPERATOR.md*) in default, and Bash(*> .claude/settings.json*) in always_on — so .env is simply missing from that set.

2. Write(.env) is denied nowhere. All four plugins list only Edit(.env):

  • plugins/laravel-forge-hermit/settings.json:19
  • plugins/claude-code-fitness-hermit/settings.json:39
  • plugins/feed-hermit/settings.json:24
  • plugins/claude-code-homeassistant-hermit/settings.json:22

Edit requires an existing file and a prior read; Write will happily create or overwrite .env wholesale. Whichever plugins keep an Edit(.env) deny should carry the matching Write(.env).

3. Pre-existing bug in the redirect patterns: they require a space after >. The glob Bash(*> .claude/settings.json*) compiles to ^.*> \.claude/settings\.json.*$, so echo x>.claude/settings.json does not match — confirmed in the table above. The same flaw applies to the OPERATOR.md pattern today. Any new .env pattern should not inherit it, and the two existing ones want fixing in the same pass.

Note the leading .* does correctly absorb the first > of >>, so append redirects are covered once the spacing is handled.

Scope, honestly

A glob list over command strings cannot fully close this. sed -i, cp, mv, dd, a Python one-liner, or an editor invocation all reach .env without matching any redirect pattern, and enforce-deny-patterns.ts deliberately fails open on parse errors (scripts/enforce-deny-patterns.ts:177, :193, :213).

So the goal here is defense-in-depth parity with how OPERATOR.md and settings.json are already treated — not a claim of a sealed boundary. Worth deciding as part of this:

  • Should the common redirect forms be patterns, or should a small dedicated hook match the write target rather than the command spelling? The latter would also cover tee, sed -i and cp, and would not need a new glob per verb.
  • Should .env writes be in default or always_on? An interactive operator editing their own .env through the agent is a legitimate flow; an always-on hermit doing it unprompted is not. That argues always_on, matching how settings.json is handled.
  • Once enforcement exists, the plugins that state "the agent cannot edit .env" can keep saying so. Until then that wording should be softened wherever it appears, since it currently overstates what is enforced.

Affected files

  • plugins/claude-code-hermit/state-templates/deny-patterns.json
  • plugins/claude-code-hermit/scripts/enforce-deny-patterns.ts (only if the target-matching option is chosen)
  • plugins/claude-code-hermit/tests/hooks.contract.test.ts — the table above is the case list
  • the four settings.json files listed above, for the Write(.env) companion deny

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghermit

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions