Skip to content

permissions.ask: the 8 shipped .env rules produce no prompt under the shipped defaultMode "auto", and .env is readable through 10 allow-listed Bash verbs regardless #1790

Description

@cristbc

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:

  1. Drop skipAutoPermissionPrompt from the shipped defaults so ask can fire.
  2. Express the .env protection as permissions.deny on the Bash read verbs scoped to that path, which binds regardless of mode.
  3. 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:

cat ~/.claude/.env

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions