GUIDE · HOW-TO 7 min ·

Minimum viable .gitignore for AI reposThe complete list. Copy and paste.

Coding agents write state files to your repo. Those files contain session history, accepted permissions, and sometimes credentials. Most .gitignore templates don't cover any of them.

TL;DR· the answer, in twenty seconds

What: Every major coding agent writes at least one state file inside your project directory. Default .gitignore templates don't cover any of them, so the files end up committed and then published.

Fix: Add the block below to your .gitignore. Mirror the same entries in .npmignore, .dockerignore, and MANIFEST.in. Add a prepublishOnly script that fails the build if any of the files exist.

Lesson: The agent that writes a "preferences" file and the agent that writes a "history" file are doing the same thing. Treat every tool's dotfile directory as a potential secrets surface until you have confirmed otherwise.

In February 2026, Knostic scanned the npm registry and found settings.local.json in roughly 1-in-13 published packages. The file is Claude Code's per-project state store. It captured environment variables from every agent session, including whatever secrets were exported in the developer's shell. Nobody exploited a zero-day. The tool did its job, git did its job, and npm publish did its job.

Three weeks earlier, Knostic had disclosed the same shape of problem with Cursor's .cursor/ directory. The directory holds chat history and sometimes cached API responses. Same mechanism: no .gitignore entry, so the directory traveled with the repo.

Anthropic patched the env-var capture in late February 2026. The file shrank. Files already committed to repos from before the patch still hold whatever they captured. The patch doesn't retroactively help any of those repos, and it covers only one tool among many.

Every agent you add to a project creates a new candidate for this problem. The fix is the same in every case.

60-second summary

  • Claude Code writes .claude/, including settings.local.json and mcp.local.json, both of which have captured env vars in the wild.
  • Cursor writes .cursor/, disclosed by Knostic in late January 2026.
  • Aider writes .aider.input.history and .aider.chat.history.md to the project root by default.
  • Codex CLI, OpenClaw, Hermes, Continue, Windsurf, and Zed's AI feature each write their own state directories.
  • None of these paths appear in the .gitignore templates that git init, GitHub, or most project scaffolding tools generate.
  • Mirror every entry in .npmignore, .dockerignore, and MANIFEST.in. Git-ignored files are still included in package tarballs unless you tell the packaging tool otherwise.

The minimum .gitignore

This block covers every major coding agent available as of April 2026. Add it to the bottom of your existing .gitignore.

# ── AI agent state (never commit) ──────────────────────────────────────────

# Claude Code (Anthropic)
.claude/
*.local.json
settings.local.json
mcp.local.json

# Cursor
.cursor/
.cursor-chat-history/

# Aider
.aider.input.history
.aider.chat.history.md

# Codex CLI (OpenAI)
.codex/

# OpenClaw / Hermes
.openclaw/
.hermes/

# Continue
.continue/

# Windsurf
.windsurf/

# Zed AI
.zed/ai/

# Model output scratch files
*.claude-output.md
prompts/scratch/

If you use GitHub's .gitignore template for your language, append this block after the generated content. It won't conflict with anything in the standard templates.

Why each line

.claude/ is the root directory Claude Code creates in your project on the first session. It holds settings.local.json, mcp.local.json, and a todos/ subdirectory. The settings.local.json file logged environment variables present during agent execution until Anthropic's late-February 2026 patch. mcp.local.json stores MCP server configuration, which can include connection strings and API endpoints.

*.local.json and settings.local.json appear as explicit entries because some projects nest settings.local.json outside the .claude/ directory. The glob also catches any future tool that adopts the .local.json naming pattern, which has spread far enough across agent tooling to be worth blocking broadly.

.cursor/ and .cursor-chat-history/ are Cursor's per-project state. Knostic's late-January 2026 disclosure found chat history and cached context in .cursor/ in public repositories. Cursor creates the directory automatically when it opens any folder.

.aider.input.history and .aider.chat.history.md are Aider's readline history and conversation log, both written to the project root by default. The chat history contains the full text of every message exchanged during a session, including file content the agent loaded as context.

.codex/ stores Codex CLI state, including task history and approved action logs.

.openclaw/ and .hermes/ are the state directories for OpenClaw and Hermes, created on first run.

.continue/ is the project-level state directory for the Continue extension. Continue runs across VS Code and JetBrains and can hold index files and conversation context.

.windsurf/ stores Windsurf editor state, including AI session context.

.zed/ai/ is the path Zed uses for its AI feature state. Blocking the parent .zed/ would pull in legitimate editor preferences; the ai/ subdirectory is the right scope.

*.claude-output.md catches scratch files that Claude Code writes when you ask it to save output to disk. These can contain arbitrary content from a session.

prompts/scratch/ is a common convention for throwaway prompt drafts. Those files often contain project internals, partial credentials used in examples, and test data. Worth blocking at the directory level.

Mirror in .npmignore, .dockerignore, and MANIFEST.in

Git-ignoring a file keeps it out of version control. It does not keep it out of published artifacts.

npm publish uses an independent include/exclude mechanism. If .npmignore doesn't exist, npm falls back to .gitignore. If .npmignore does exist, it takes over entirely and .gitignore stops applying to the tarball. The moment you create .npmignore for any reason, every agent state path needs an explicit entry there too.

# .npmignore -- add these alongside your existing entries
.claude/
.cursor/
.cursor-chat-history/
.aider.input.history
.aider.chat.history.md
.codex/
.openclaw/
.hermes/
.continue/
.windsurf/
.zed/
*.local.json
*.claude-output.md
prompts/scratch/

For Python packages, add these lines to MANIFEST.in:

# MANIFEST.in
recursive-exclude .claude *
recursive-exclude .cursor *
recursive-exclude .aider *
recursive-exclude .codex *
recursive-exclude .openclaw *
recursive-exclude .hermes *
recursive-exclude .continue *
recursive-exclude .windsurf *
recursive-exclude .zed *
global-exclude *.local.json
global-exclude *.claude-output.md

For Docker images, add to .dockerignore:

# .dockerignore
.claude/
.cursor/
.cursor-chat-history/
.aider.input.history
.aider.chat.history.md
.codex/
.openclaw/
.hermes/
.continue/
.windsurf/
.zed/
*.local.json
*.claude-output.md
prompts/scratch/

The COPY . . pattern in a Dockerfile copies every file in the build context, including dotfiles. .dockerignore is the only thing standing between an agent state file and a published container image.

Add a prepublishOnly script to package.json as a last-resort check:

{
  "scripts": {
    "prepublishOnly": "test ! -e .claude/settings.local.json && test ! -e .cursor && test ! -e .aider.chat.history.md"
  }
}

This fails the publish if any of those paths exist, even if they somehow slipped through .npmignore. Run the same check in CI, not only as a pre-commit hook. Pre-commit accepts --no-verify. CI does not.

What .gitignore can't fix

A .gitignore entry stops future commits. It does nothing for files already committed.

If an agent ran in your repo before you added these entries, check git history:

git log --all --oneline -- '.claude/*' '.cursor/*' '.aider*'

Any output means the file was tracked. Inspect the content:

git log -p -- '.claude/settings.local.json'

If you find API keys, bearer tokens, or database URLs in the output, treat those credentials as leaked. Rotate them. Deleting the file in a subsequent commit leaves the value in git history. Rewriting git history removes it from the repo but not from any tarball already published to npm or PyPI. The registry treats published versions as immutable.

For npm packages, pull the actual tarball and check:

npm pack <your-package-name>@<version>
tar -tf <your-package-name>-<version>.tgz | grep -E 'claude|cursor|aider|codex'

Repeat for every version published since the first agent session in the repo.

The part most guides skip

The received wisdom is "add the dotfiles to .gitignore and you're done." That's the wrong frame.

Agent state files are a symptom. The underlying condition is that coding agents inherit your full shell environment, read your project directory, and write observations back to disk in a location you might not inspect. The specific filenames matter less than the class of behavior.

Anthropic's late-February patch shrank settings.local.json. It did not change the fact that the agent still sees whatever secrets are in your shell environment during a session. A future agent, or a future version of an existing one, might write a different file with a different name. The .gitignore block above covers the tools that exist today. It will not cover tools that ship next quarter.

A broker like hasp addresses this at the source. If the agent inherits a reference rather than a real value, the state file it writes holds the reference. The next tool that ships with a new state path writes the same inert string. You still want the .gitignore entries; you just stop depending on them as the primary control.

The durable fix is to keep sensitive values out of the environment the agent inherits, not just out of the files it writes. That means project-scoped secret delivery at execution time, not long-lived exports in ~/.zshrc. The .gitignore is necessary. It's not sufficient.

Checklist for your next PR

## AI agent state audit

- [ ] .claude/, .cursor/, .aider*, .codex/, .openclaw/, .hermes/, .continue/, .windsurf/, .zed/ai/ in .gitignore
- [ ] Same paths in .npmignore (if package is published to npm)
- [ ] Same paths in MANIFEST.in (if published to PyPI)
- [ ] Same paths in .dockerignore (if building Docker images)
- [ ] prepublishOnly script in package.json fails if state files exist
- [ ] git log --all -- '.claude/*' clean (no prior commits of agent state)
- [ ] npm pack of last 3+ versions checked for agent state files
- [ ] No credentials visible in git log -p output for any agent state path
- [ ] Rotated any credentials found in prior commits
- [ ] CI step that greps for agent state files before publish

Paste this into a PR template or SECURITY.md. Re-run it whenever a new agent tool lands in the repo.

What this means for your stack

The .gitignore block above closes the obvious hole. Past that, you're still managing secrets that live as long-lived shell variables, which agents read and which state files reflect. The .gitignore keeps those reflections out of git. It doesn't stop them from being written to disk, and it doesn't stop a future tool from writing to a path you haven't blocked yet.

The structural fix is a runtime model where secrets don't sit in the agent's inherited environment in the first place: an encrypted local vault, per-session grants to specific child processes, and an audit log that records every access. hasp is one working implementation. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, connect a project, hand the next session a reference instead of a key. Source-available (FCL-1.0), local-first, macOS and Linux, no account.

Whether you adopt a broker or not, the .gitignore is the floor. Any repo where an agent has run needs these entries before the next git add -A.

Sources· cited above, in one place

NEXT STEP~90 seconds

Stop handing the agent your real keys.

hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.

  • Local, encrypted vault — no account, no cloud, no telemetry by default.
  • Brokered run — agent gets a reference, the child process gets the value.
  • Pre-commit + pre-push hooks catch managed values before they ship.
  • Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
→ okvault unlocked · binding ./api
→ okgrant once · pid 88421
→ okagent never read

macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.

Browse all clusters· eight threads, one index