Summary
LifeOS/install/settings.system.json ships eight permissions.ask entries, all guarding .env:
Read(~/.claude/.env) Edit(~/.claude/.env)
Read(~/.claude/.env.*) Edit(~/.claude/.env.*)
Read(~/.claude/**/.env) Edit(~/.claude/**/.env)
Read(~/.claude/**/.env.*) Edit(~/.claude/**/.env.*)
The same file also ships permissions.defaultMode: "auto" and skipAutoPermissionPrompt: true. Under that combination the ask tier does not produce a prompt: a Read of ~/.claude/.env returns cleartext with no interruption. The rule does not fire on the exact tool path it was written for.
A second, independent bypass
Permission rules bind to a tool name. The ask entries name only Read and Edit, while permissions.allow ships ten Bash verbs that reach the same bytes:
$ jq -r '.permissions.allow[] | select(test("^Bash\\((cat|rg|grep|sed|head|tail|awk|less|xxd|od):"))' \
LifeOS/install/settings.system.json
Bash(grep:*) Bash(rg:*) Bash(sed:*) Bash(awk:*) Bash(head:*)
Bash(tail:*) Bash(cat:*) Bash(less:*) Bash(xxd:*) Bash(od:*)
So cat ~/.claude/.env is auto-approved even in a mode where the ask tier does work.
Impact
The shipped configuration reads as though the credential store is gated. It is not, by two independent mechanisms. Anyone auditing their install by reading settings.system.json reaches the wrong conclusion — which is worse than shipping no rule at all, because a dead gate stops people looking.
Suggested fix
Any one of:
- Drop
skipAutoPermissionPrompt from the shipped defaults so ask can fire.
- Express the
.env protection as permissions.deny on the Bash read verbs scoped to that path, which binds regardless of mode.
- Remove the eight entries and document that
.env is not gated by the permission layer.
Whichever is chosen, the tool-name/Bash asymmetry is worth a line in the docs. It is not obvious that a Read() rule leaves cat untouched.
What this looks like on a stock install
Kai is asked to work out why an API call is failing. The natural first move is to check whether the key is set:
That is auto-approved by Bash(cat:*). No prompt. The credentials land in transcript context, where they persist for the rest of the session and into any transcript-derived memory or harvest path.
The eight ask rules that look like they exist to prevent exactly this do not fire, for either of the two reasons above.
And this is not an exotic workflow. ~/.claude/.env is not the process environment — it is a file that Bun/Node tools choose to read. Any compiled CLI in the stack (a Go or Rust binary) never sees it, so getting a secret to one means reading the file in the shell and exporting it for that invocation. The shell read is the normal path, not an edge case, which is why gating only Read/Edit leaves the common case uncovered.
Summary
LifeOS/install/settings.system.jsonships eightpermissions.askentries, all guarding.env:The same file also ships
permissions.defaultMode: "auto"andskipAutoPermissionPrompt: true. Under that combination theasktier does not produce a prompt: aReadof~/.claude/.envreturns cleartext with no interruption. The rule does not fire on the exact tool path it was written for.A second, independent bypass
Permission rules bind to a tool name. The
askentries name onlyReadandEdit, whilepermissions.allowships ten Bash verbs that reach the same bytes:So
cat ~/.claude/.envis auto-approved even in a mode where theasktier does work.Impact
The shipped configuration reads as though the credential store is gated. It is not, by two independent mechanisms. Anyone auditing their install by reading
settings.system.jsonreaches the wrong conclusion — which is worse than shipping no rule at all, because a dead gate stops people looking.Suggested fix
Any one of:
skipAutoPermissionPromptfrom the shipped defaults soaskcan fire..envprotection aspermissions.denyon the Bash read verbs scoped to that path, which binds regardless of mode..envis not gated by the permission layer.Whichever is chosen, the tool-name/Bash asymmetry is worth a line in the docs. It is not obvious that a
Read()rule leavescatuntouched.What this looks like on a stock install
Kai is asked to work out why an API call is failing. The natural first move is to check whether the key is set:
That is auto-approved by
Bash(cat:*). No prompt. The credentials land in transcript context, where they persist for the rest of the session and into any transcript-derived memory or harvest path.The eight
askrules that look like they exist to prevent exactly this do not fire, for either of the two reasons above.And this is not an exotic workflow.
~/.claude/.envis not the process environment — it is a file that Bun/Node tools choose to read. Any compiled CLI in the stack (a Go or Rust binary) never sees it, so getting a secret to one means reading the file in the shell and exporting it for that invocation. The shell read is the normal path, not an edge case, which is why gating onlyRead/Editleaves the common case uncovered.