Home › Articles

How to stop Claude Code, Cursor and Codex reading your .env file

Updated 2026-10-27

In short: .gitignore doesn't keep secrets away from a coding agent, and neither does a line in CLAUDE.md or AGENTS.md. Each tool has its own control. Claude Code uses Read deny rules plus the sandbox, Cursor uses .cursorignore plus a hook for terminal commands, and Codex uses a permission profile with deny globs. Each one leaves a gap, listed below. The real fix is not having production keys in the working tree.

Why "don't read .env" in your rules isn't enough

A rule in CLAUDE.md, AGENTS.md or a Cursor rule is guidance. The model usually follows it, but nothing enforces it. The Claude Code permissions docs say it directly: permission rules are enforced by Claude Code, not by the model, and instructions in your prompt or CLAUDE.md shape what Claude tries to do without changing what Claude Code allows. The same split exists in Cursor and Codex. Keep the rule (our security rules checklist covers what to write), and add the tool-level block below so a slip doesn't put a key into the conversation.

Three things people try that don't work:

Claude Code: Read deny rules, then the sandbox

1. Deny the files

Put this in the project's .claude/settings.json and commit it, so everyone on the repo gets it:

{
  "permissions": {
    "deny": [
      "Read(.env)",
      "Read(.env.*)",
      "Read(!.env.example)",
      "Read(*.pem)",
      "Read(*.key)"
    ]
  }
}

How these match, per the path pattern docs:

To cover every project on your machine, put rules in ~/.claude/settings.json with a // (filesystem root) or ~/ (home) anchor. A single leading / in user settings means ~/.claude/, not your project, so Read(/secrets/**) there blocks ~/.claude/secrets. The docs give Read(//**/.env) as the pattern for any .env on the filesystem.

2. Know what deny rules don't cover

The docs are explicit. Read deny rules apply to Claude's built-in file tools (Read, Grep, Glob, best effort), @file mentions, IDE selection context, the Bash file commands Claude Code recognizes (cat, head, tail, sed, tee) and redirect targets like < .env. They don't apply to:

3. Turn on the sandbox for shell commands

The sandbox is the OS-level layer for Bash commands and every process they start (Seatbelt on macOS, bubblewrap on Linux and WSL2; native Windows runs commands unsandboxed). It's off by default. Run /sandbox in a session, or set sandbox.enabled in settings. By default sandboxed commands can still read most of the machine, but the sandbox docs say paths from your permission rules, including Read and Edit deny rules, are merged into the sandbox configuration. So with the sandbox on, the .env rules above also apply to that Python one-liner.

A user-level setup in ~/.claude/settings.json:

{
  "permissions": {
    "deny": [
      "Read(//**/.env)",
      "Read(//**/.env.*)",
      "Read(~/.ssh/**)",
      "Read(~/.aws/**)"
    ]
  },
  "sandbox": {
    "enabled": true,
    "allowUnsandboxedCommands": false,
    "credentials": {
      "envVars": [
        { "name": "OPENAI_API_KEY", "mode": "deny" },
        { "name": "STRIPE_SECRET_KEY", "mode": "deny" }
      ]
    }
  }
}

Check it: run /permissions to see your deny rules and which file each came from. Then ask Claude to show you the contents of .env, and to run a one-line script that prints it. Both should fail.

Cursor: .cursorignore, plus a hook for the terminal

1. Ignore the files

Per the ignore file docs, files in .cursorignore are blocked from Agent, Tab and @-mention references. It uses gitignore syntax, including ! to re-include a file:

# .cursorignore  (gitignore syntax)
.env
.env.*
!.env.example
*.pem
*.key

To cover every project, add the same patterns (**/.env, **/.env.*, **/*.pem, **/*.key) to the global ignore list in your user settings. The docs note it's empty by default.

Don't skip this because the docs also list .env* in Cursor's default ignore list. In a forum thread (March 2026), a Cursor team member explained that .gitignore and the default ignore list applied to indexing, not to the agent's Read tool or Tab, and pointed to .cursorignore as the control for agent access. An explicit entry works either way.

2. Cover the terminal with a hook

The same docs say the terminal and MCP tools used by Agent can't honor .cursorignore, so cat .env in a shell command still works. The agent security docs add that terminal commands need your approval by default. If you've allowlisted commands or use a run mode that approves them for you, add a beforeShellExecution hook. Cursor runs it before each shell command and accepts allow, deny or ask.

.cursor/hooks.json in the project:

{
  "version": 1,
  "hooks": {
    "beforeShellExecution": [
      {
        "command": ".cursor/hooks/block-secrets.py",
        "matcher": "\\.env|\\.pem|\\.key|id_rsa|id_ed25519",
        "failClosed": true
      }
    ]
  }
}

The matcher is a regex tested against the full command string, so the hook only runs for commands that mention a secret-looking name. Every other command never reaches it and keeps its normal approval flow. failClosed: true blocks the command if the script crashes or times out. Without it, Cursor lets the command through on those failures.

.cursor/hooks/block-secrets.py (run chmod +x on it; project hooks run from the project root):

#!/usr/bin/env python3
"""Cursor beforeShellExecution hook: deny shell commands that name a secrets file."""
import json, re, sys

cmd = json.load(sys.stdin).get("command", "")
# .env, .env.local, config/.env.production, *.pem, *.key, id_rsa, id_ed25519
SECRET = re.compile(r"(?<![\w.-])(\.env(\.[\w-]+)*|[\w.-]+\.(pem|key)|id_(rsa|ed25519))(?![\w.-])")
SAFE = {".env.example", ".env.sample", ".env.template"}
hits = [m.group(0) for m in SECRET.finditer(cmd) if m.group(0) not in SAFE]

if hits:
    print(json.dumps({
        "permission": "deny",
        "user_message": f"Blocked: command touches {', '.join(sorted(set(hits)))}",
        "agent_message": "That command reads or writes a secrets file, which is blocked. Use .env.example "
                         "for variable names and ask the user for anything else you need.",
    }))
else:
    print(json.dumps({"permission": "ask"}))  # no secret named: leave the decision to Cursor's normal approval

It denies the command when it names .env, .env.local, config/.env.production, a .pem or .key file, or an SSH key. .env.example and lookalikes such as .envrc fall through to ask, which hands the decision back to Cursor's normal approval. Writing to .env (cp .env.example .env) is denied too, so do that step yourself. We tested the script on 13 sample commands with Python 3, using the input format from the hooks reference, but not inside Cursor itself. Watch the Hooks output channel the first time it fires. On Windows, Cursor runs hook commands through PowerShell, so the shebang line won't work there. Change the command to call python explicitly.

It's a text match, so it has the same blind spot as Claude Code's deny rules: a script that opens .env without naming it in the command isn't caught. Treat it as a guard against the common case, not a boundary.

Codex: a permission profile with deny globs

Codex enforces file access with its sandbox, and the permission profile docs describe how to deny reads inside a writable workspace. Profiles are marked beta. Put this in ~/.codex/config.toml:

# ~/.codex/config.toml
default_permissions = "no-secrets"

[shell_environment_policy]
ignore_default_excludes = false   # drop env vars whose names contain KEY, SECRET or TOKEN

[permissions.no-secrets]
description = "Workspace writes, no .env or key files"
extends = ":workspace"

[permissions.no-secrets.filesystem]
glob_scan_max_depth = 4
"~/.ssh" = "deny"

[permissions.no-secrets.filesystem.":workspace_roots"]
"**/.env" = "deny"
"**/.env.*" = "deny"
"**/*.pem" = "deny"
"**/*.key" = "deny"

Check it: start a new session, ask Codex to print .env, and confirm it can't show you the contents.

The fix that doesn't depend on any tool

Every control above has a gap, and they all assume you remembered to set them up on this machine. Make the file less worth stealing:

  1. Keep production secrets off your laptop's working tree. Use development-only keys locally, with low limits and no access to real customer data.
  2. Load real secrets at run time from your platform's secrets store or a secrets manager, not from a file in the repo.
  3. Commit a .env.example with every variable name and no values. The agent gets what it needs to write code without seeing a key.
  4. Rotate any key an agent has printed. Once a value is in a transcript, treat it as exposed.
  5. Watch for copies. Worktree setups often copy .env into each new checkout. Commit .claude/settings.json, .cursorignore and .cursor/hooks.json so every worktree gets them, and keep the Codex profile in ~/.codex/config.toml, where it applies to whatever workspace you open. See parallel agents with git worktrees.

Five-minute checklist

  1. Claude Code: Read(.env), Read(.env.*) and Read(!.env.example) in .claude/settings.json; /sandbox on; strict sandbox mode in user settings.
  2. Cursor: .env and .env.* in .cursorignore and in the global ignore list; the beforeShellExecution hook if any commands run without your approval.
  3. Codex: a :workspace-based profile with .env deny globs, selected with default_permissions; ignore_default_excludes = false.
  4. Keep the "never read or print secrets" line in your always-on rules as the explanation, not the enforcement.
  5. Test each tool by asking it to show you .env. Then rotate anything that ever leaked.

Rules files tell the agent what you want, and settings make it stick. The same split applies to tests and formatting; see why a long CLAUDE.md gets ignored for a Stop hook that blocks "done" while tests fail.

FAQ

How do I stop Claude Code from reading my .env file?

Add Read deny rules to .claude/settings.json, for example Read(.env) and Read(.env.*), which match at any depth under the folder you start Claude in. A .claudeignore file has no effect. Deny rules cover Claude's file tools and the Bash commands Claude Code recognizes, such as cat and head, but not a script that opens the file itself, so also turn on the sandbox with /sandbox; Claude Code merges Read deny rules into the sandbox's filesystem rules.

Does Cursor read .env files by default?

Don't rely on the default. The docs list .env* in Cursor's default ignore list, but a Cursor team member explained on the forum that the default list and .gitignore applied to indexing, not to the agent's file reads. Put .env and .env.* in .cursorignore or in the global ignore list. Neither stops the agent's terminal or MCP tools, so add a beforeShellExecution hook for commands that name the file.

How do I block .env files in Codex?

Use a permission profile in ~/.codex/config.toml that extends :workspace and sets a deny glob such as "**/.env" = "deny" under :workspace_roots, then select it with default_permissions. Profiles are beta, don't combine with sandbox_mode, and only govern the local commands Codex runs in its sandbox.

Is .gitignore enough to keep secrets away from an AI coding agent?

No. .gitignore keeps a file out of git, not out of the agent's reach: the agent works on your disk and can open the file anyway. In Cursor, staff described .gitignore as an indexing control. Use each tool's own deny mechanism: Read deny rules in Claude Code, .cursorignore in Cursor, and deny globs in a Codex permission profile.

What's the safest way to keep API keys away from coding agents?

Don't have production keys in the working tree at all. Use development-only keys locally, load real secrets from a secrets manager at run time, commit a .env.example with names but no values, and rotate any key an agent has printed. The tool settings in this article reduce accidents; they aren't a vault.

Sources

Official docs, checked 2026-10-27.

Get the free rules sampler + new tips by email

Confirm your email and we'll send you straight to the sampler download. Then one short email every two weeks covering tool format changes (Cursor, Claude Code, Codex, Windsurf, Copilot), new rules and kit updates. No spam.

We store only your email, with our email provider Resend. See the privacy policy.

Unsubscribe anytime from any email. Rather skip email? Get the sampler on GitHub or download the zip directly.