Claude Code's git reset --hard Can Silently Delete Your Gitignored .env — Here's the Fix

Claude Code's git reset --hard Can Silently Delete Your Gitignored .env — Here's the Fix

Todd Mercer

A small luminous amber kernel rests untouched inside a translucent curved vessel while a broad charcoal-gray tide sweeps past it.

If an agent session ever runs git checkout <branch> followed by git reset --hard origin/<branch> in your working tree, and your gitignored .env happens to share a path with a file that branch's history once tracked, git will overwrite that .env without asking and then delete it on the reset — with no recycle bin, no confirmation, and often no local backup. This is not a Claude Code bug; it is git's documented default behavior, and it can bite any agent (or human) that runs those two commands back to back. Protect the file by restricting direct reads and writes, requiring review before destructive Git operations, and keeping a backup outside the working tree. The deny rules below reduce direct tool access; the Git command policy is an instruction, not an enforced block on Git side effects.

Next action: install Aident Loadout once, then run a bounded, read-only sandbox check of your own repo before you trust any agent with git reset or git checkout:

Follow https://aident.ai/SETUP.md
aident capabilities search --query "run terminal command in sandbox" --types '["action"]'

Preflight the exact Action, then run this against your own clone: git log --all --oneline -- .env (any output means .env is tracked somewhere in your history, which is the exact precondition for this failure) and grep -c '"deny"' .claude/settings.json (zero means you have no deny rule yet). Both checks are read-only; use the results to decide whether to add the rules below.

The Exact Failure

GitHub issue anthropics/claude-code#104331, filed October 22, 2026 — by Claude Code itself, at the user's explicit request, after the in-app feedback button and browser-based claude.ai login were both unavailable — lays out the full git reflog timeline:

  1. The user asked the agent to connect an existing local workspace to a private GitHub repository, rewrite the README, and set up hosting. They did not ask for any reset, checkout, cleanup, or deletion.

  2. The agent moved the project's .git directory up to the workspace root, preserving history, and committed. That preserved history happened to track a file at the root path .env from an earlier point in the project.

  3. The workspace root also held a separate, untracked, gitignored .env with the user's live API keys — unrelated to the old tracked copy, just colliding on the same path.

  4. The agent opened and merged a pull request into main, then ran, unprompted, a single chained "sync local main" command: git fetch -q origin && git checkout -q main && git reset -q --hard origin/main && git log --oneline -4.

  5. git checkout -q main silently overwrote the gitignored .env with the old tracked version, because the local main ref still pointed at the commit that tracked that path.

  6. git reset -q --hard origin/main then deleted the file outright, because the new main no longer tracks it at all.

  7. The user noticed about fifteen minutes later. Keys for five separate services — an advertising API, a weather API, a public-data API, a cloud-vision API, and an LLM API — were gone, with no editor local history, no Time Machine backup, no shell-history trace, and no agent-transcript record of the values. The user had to regenerate every key from scratch.

The repository's own triage labeled the report data-loss, area:security, and area:permissions on filing. As of this writing the issue is open with no maintainer response yet, so treat the underlying git behavior — not a pending patch — as the thing to defend against.

Why Git Does This on Purpose

Neither step is a malfunction. Both are documented, long-standing git behavior that predates agentic coding tools entirely:

  • git-checkout documentation, under --overwrite-ignore: "Silently overwrite ignored files when switching branches. This is the default behavior. Use --no-overwrite-ignore to abort the operation when the new branch contains ignored files." Git's untracked-file safety check — the one that normally stops a checkout from clobbering your work — explicitly does not cover ignored files. A gitignored file sitting at a path the target branch tracks is treated as expendable.

  • git-reset documentation, under --hard: "Overwrite all files and directories with the version from <commit>, and may overwrite untracked files. Tracked files not in <commit> are removed so that the working tree matches <commit>." Once the checkout step above has made .env tracked again (by overwriting it with the old tracked copy), a subsequent reset --hard to a commit where .env isn't tracked removes it as part of making the working tree match.

The trap only fires when both conditions line up: your currently gitignored secret file shares a path with something some reachable commit once tracked, and something runs checkout followed by reset --hard without pausing. That combination is easy to hit by accident the moment a repository's .git directory gets moved, restructured, or re-rooted — exactly what the agent was doing in issue #104331 — because restructuring often resurrects old tracked paths that current .gitignore rules were never written to anticipate.

This is distinct from the more commonly discussed failure where an agent intentionally rewrites .env with placeholder values while "cleaning up" a project or filling in a template; that's a model judgment call about file contents. What's described here has nothing to do with judgment — it's git's plumbing doing exactly what the man pages say, triggered by a command sequence that never needed to touch the working tree at all.

The Fix: Four Layers

No single control here is sufficient alone — stack them.

1. Deny the exact tool calls in .claude/settings.json. Claude Code's own settings reference documents permissions.deny with this literal example:

{
  "permissions": {
    "deny": ["Read(./.env)", "Read(./.env.*)"]
  }
}

Extend it to cover writes and your real path, and add a matching Bash deny since deny only blocks Claude Code's built-in Read/Write/Edit tools, not a shell command that touches the same file:

{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Write(./.env)",
      "Write(./.env.*)",
      "Edit(./.env)",
      "Edit(./.env.*)",
      "Bash(cat ./.env*)",
      "Bash(rm ./.env*)"
    ]
  }
}

Commit this to the project's .claude/settings.json so the whole team shares it; use .claude/settings.local.json only for personal overrides. The built-in tool rules restrict direct access through those tools. The two Bash patterns are best-effort examples for those exact command shapes, not a filesystem boundary: other shell commands can still touch the same path. None of these file rules prevents a git checkout/reset side effect; layer two is a command-review policy, not an enforced Git deny rule.

2. Replace the destructive sync with the non-destructive one. The GitHub issue's own root-cause section names the fix: git branch -f main origin/main updates where the local main ref points without touching the working tree at all, so it can't overwrite or delete anything sitting in it. If an agent (or a script) only needs the local branch pointer to catch up to the remote, that command — not checkout plus reset --hard — is the correct tool. Use git branch -f only for a branch that is not checked out in any worktree, and inspect its local-only commits first: it can discard the branch reference to those commits even though it leaves files alone. If you are already on main, git merge --ff-only origin/main is the normal fast-forward operation, but it updates tracked working-tree files; review and preserve local work before running it. Put the command-review rule in your project's CLAUDE.md or AGENTS.md:

## Git safety
- Never run `git reset --hard`, `git checkout <branch other than the current one>`,
  `git clean -f`, or `git stash` without stating the exact command and getting
  explicit confirmation first.
- For a branch not checked out in any worktree, inspect local-only commits
  before considering `git branch -f <branch> origin/<branch>`. This moves
  the branch ref without touching files, but can discard local branch history.
- On the current branch, use `git merge --ff-only origin/<branch>` only after
  reviewing local work; it updates tracked working-tree files

This is a soft guardrail — an instruction file, not an enforced boundary — so treat it as one layer among several, not the whole defense.

3. Keep a copy of your secrets outside the working tree. The single point of failure in issue #104331 was that the live keys existed in exactly one place: an ignored file inside the directory the agent was restructuring. A secret manager, your deploy platform's environment-variable panel, or even a password manager entry gives you a second copy that a local git operation can't reach. If you want agents to read and inject credentials without ever holding or exposing the raw values in the project directory, see our guide to giving AI agents API access without exposing keys.

4. Confirm the deny rule is actually what's blocking the earlier, more common symptom. If you've hit a prompt asking to approve reading .env rather than a deletion, that's a different, already-covered situation — see our guide to the Read(.env) permission prompt for preserving that deny rule without false positives.

If the agent you're scoping also has standing access to connected SaaS tools, not just the local filesystem, the same "least-surprise default, most-destructive outcome" pattern shows up there too; see our AI agent access-control matrix across Slack, Drive, Notion, and Jira for scoping that wider surface.

Recover If It Already Happened

Do these in order; stop as soon as one source has your real values.

  1. git reflog — if any commit in your reflog still has .env tracked with the real values (for example, right after the overwriting checkout but before the reset --hard), git show <that-commit>:.env recovers it. This only works if you catch it before further commands move the ref further and git garbage-collects the blob.

  2. Your editor's local file history — VS Code and most Cursor-based editors keep an in-editor history of saved file versions independent of git; check it for .env if the file was ever opened and saved in the editor.

  3. Your deploy platform's environment-variable panel (Vercel, Netlify, Railway, your cloud provider's secret manager) — these are often the most reliable second copy, since most teams set the same keys there to deploy.

  4. Shell history — grep -i "API_KEY=" ~/.zsh_history or ~/.bash_history for a past export or .env paste.

  5. Regenerate any key you can't recover from the above. Treat a destroyed key as compromised-by-default rather than spending more time chasing a copy that may not exist.

Sources and Method

  • anthropics/claude-code issue #104331, filed October 22, 2026, self-reported by Claude Code at the user's request, with the full git reflog timeline, environment, and impact.

  • git-checkout reference, --overwrite-ignore / --no-overwrite-ignore, confirming silent overwrite of ignored files on branch switch is the documented default.

  • git-reset reference, --hard, confirming it removes tracked files absent from the target commit.

  • Claude Code settings reference, permissions.deny, confirming the exact Read(./.env) / Read(./.env.*) deny-rule syntax used above.

About the author

Illustrated editorial avatar of Todd Mercer

Todd Mercer

Todd Mercer is a senior engineer with more than 10 years of experience working on social media platforms and large-scale ordering systems. He graduated from the University of Illinois Urbana-Champaign in computer science. At Aident, he covers technical issues, developer tools, and technology topics.

Home

Home

Home

Integrations

Integrations

Integrations

Vault

Vault

Vault

Audit

Audit

Audit

Arana Grande

Arana Grande

Arana Grande

Free

Free

Free

30-day audit summary

30-day audit summary

30-day audit summary

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

View Audit

View Audit

View Audit

Loadout usage

Loadout usage

Loadout usage

617 action calls in the last 30 days

617 action calls in the last 30 days

617 action calls in the last 30 days

May 19 - Jun 17

May 19 - Jun 17

May 19 - Jun 17

10 active days

10 active days

10 active days

Less

Less

Less

More

More

More

Recent activity

Recent activity

Recent activity

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Shopify

Shopify

Shopify

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Success

Success

Success

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Success

Success

Success

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Failed

Failed

Failed

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Connected integration coverage

Connected integration coverage

Connected integration coverage

162

162

162

of 753 accessible connected

of 753 accessible connected

of 753 accessible connected

Callable actions

Callable actions

Callable actions

1,126

1,126

1,126

Vault credentials

Vault credentials

Vault credentials

8

8

8

Explore what's possible

Explore what's possible

Explore what's possible

See all Integrations

See all Integrations

See all Integrations

Google Ads

Google Ads

Google Ads

All available Goolge Ads tools via...

All available Goolge Ads tools via...

All available Goolge Ads tools via...

X (twitter)

X (twitter)

X (twitter)

All available X tools via...

All available X tools via...

All available X tools via...

Github

Github

Github

All available Github tools via...

All available Github tools via...

All available Github tools via...

Notion

Notion

Notion

All available Notion tools via...

All available Notion tools via...

All available Notion tools via...

Slack

Slack

Slack

All available Slack tools via...

All available Slack tools via...

All available Slack tools via...

Firecrawl

Firecrawl

Firecrawl

All available Firecrawl tools via...

All available Firecrawl tools via...

All available Firecrawl tools via...

753 integrations are available for loadouts.

753 integrations are available for loadouts.

753 integrations are available for loadouts.

The one tool

for every tool

your agent needs.

Give any AI agent real capabilities in seconds. Connect 27,000+ tools once, skip the setup headache, and let your agents execute.

Try Aident Loadout

Empower your Codex or OpenClaws to get real jobs done. Connect 27,000+ tools in one prompt, and let your agents deliver real results.

Try Aident Loadout

Empower your Codex or OpenClaws to get real jobs done. Connect 27,000+ tools in one prompt, and let your agents deliver real results.

Try Aident Loadout

Empower your Codex or OpenClaws to get real jobs done. Connect 27,000+ tools in one prompt, and let your agents deliver real results.