Engineering 17 min read

You Clicked \"Don't Ask Again\" in Your Vault. The Approval Is Saved as Text — and Your File Sync Doesn't Read Git's Ignore List

MMNMNOTE
ai-agentssecurityfile-syncgitignorelocal-first

When you tell an AI agent "yes, and don't ask again" for a shell command, it saves that exact command — word for word, including any password or token it contained — as a plain-text rule in a local config file. The agent keeps that file out of git. Your file-sync service has never heard of git's ignore list.

This gap is not a bug in any one tool. It is the intersection of three independent systems — an agent, a version-control daemon, and a sync client — each doing exactly what it was designed to do. Understanding where their responsibilities diverge is the whole job.


What "don't ask again" actually stores

When Claude Code asks permission to run a Bash command and you choose "Yes, and don't ask again," Claude Code saves that approval as an allow rule in .claude/settings.local.json.1 If you start Claude Code in a subdirectory of a git repository, it reads and writes that file at the repository root and applies the approval across the whole repository.1

The file is plain JSON. A session that approved a database migration command might leave behind a rule like this:

{
  "permissions": {
    "allow": [
      "Bash(PGPASSWORD=hunter2 psql -U deploy -d production -f migrate.sql)"
    ],
    "deny": []
  }
}

That string is stored verbatim. Every subsequent session that opens that repository will see the rule and can run the same command without re-prompting. The convenience is real. So is the exposure.

HN commenter Aurornis put the precise condition in one sentence: "If you commit your local Claude settings and in those settings you have manually approved commands containing secrets directly in the command and then you publish the repo, that's exposed."2

Apiiro researchers searching public GitHub between November 1 and 3, 2026, found 104,185 copies of .claude/settings.local.json. Of those, 1,122 — roughly 1.1% — held a real secret: a key in a known provider format, or a password or token next to a public host.3 Generic scanners missed many of them: of the 1,122, gitleaks flagged 564 and TruffleHog 643, and 215 were flagged by neither.3 In 539 of the 1,105 repositories with a real secret that Apiiro traced, a commit co-authored by Claude changed the file.3


Why git's protection doesn't reach your sync client

The first time Claude Code writes .claude/settings.local.json in a git repository that doesn't already ignore it, it adds **/.claude/settings.local.json to your global git excludes file, so the file stays out of your commits in every repository.1 The global excludes file is core.excludesFile — by default $XDG_CONFIG_HOME/git/ignore or ~/.config/git/ignore.1

That protection is real, and it covers the common case. Why the exposed copies got committed anyway is disputed. A Hacker News reply signed "Boris from the Claude Code team" says the file is gitignored when Claude creates it, so the leaked copies must have been created manually and checked in.4 Apiiro's answer is that a global ignore list doesn't cover a folder that becomes a repo later, a clone on another machine, or a file someone already committed — and ignore rules never apply to a file git already tracks.3 Either way, the ignore list lives on one machine and is read by one program.

But git's excludes mechanism is defined with precision: "Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user's editor) generally go into a file specified by core.excludesFile in the user's ~/.gitconfig."5 The key word is "Git." The git manual's gitignore(5) page says these patterns apply to untracked files only — the program reading them is git, and git alone.5

A file-sync client is not git. It has its own directory-walking logic, its own ignore-file format, and its own idea of what it should copy. When Dropbox, iCloud Drive, OneDrive, Syncthing, or an Obsidian sync plugin runs its next sync cycle, it does not consult ~/.config/git/ignore. It consults its own rules — if it has any.

Your folder
├── .claude/
│   └── settings.local.json   ← git never sees it
│                               ← sync client copies it anyway
├── notes/
│   └── ...
└── .git/
    └── ...

The three programs each enforce only their own rules, and their responsibilities never overlap:

graph TD
    A["AI Agent"] -->|"writes approval rule"| F[".claude/settings.local.json"]
    F --> G["Git"]
    G -->|"global excludes — never committed"| GX["Excluded from version control"]
    F --> S["Sync client"]
    S -->|"reads its own rules only"| SC["Copied to cloud or second device"]

Figure 1: The three-layer gap. Git's global excludes keep the approval file out of version control. The sync client, operating an independent code path, copies whatever it finds unless its own rules say otherwise.

The result is that a file git will never commit can still travel from your laptop to your phone, your partner's computer, or a cloud service's servers, via a completely independent code path.


Per-sync-client ignore mechanisms

Each sync client has its own documented mechanism for excluding files. None of them read git's global excludes.

Syncthing uses a file called .stignore at the root of each synced folder. The file is never synced itself, so ignore rules are local to each device.6 To exclude the approval file, add a line:

.claude/settings.local.json

Dropbox does not use a dotfile pattern list. Its ignore mechanism is a per-item extended attribute: set com.dropbox.ignored on the file or folder, and Dropbox removes it from the server and from all other devices.7 Dropbox's own documentation confirms there is no .dropboxignore file — a common misconception.7 The documented never-synced list (desktop.ini, thumbs.db, .DS_Store) contains no general dotfile rule.8

Obsidian Sync excludes dot-prefixed folders by default, except .obsidian itself. The selective-sync excluded list does not remove files that are already synced.9

remotely-save skips files and folders whose names begin with . or _ by default; obsidian-livesync offers optional hidden-file sync that is off by default.10

Git-based sync (the obsidian-git plugin, with 3,422,527 downloads as of November 2026)11 is the only sync path that also reads git's ignore machinery — .gitignore, .git/info/exclude, and the global excludes file all apply, so .claude/settings.local.json is excluded from commits and therefore never pushed. This is the safest default, and the reason the exposure is primarily a sync-client gap rather than a git-commit gap.

The rule to remember: each sync tool enforces only its own rules. Knowing that git excludes a file tells you nothing about whether Dropbox, iCloud, or a plugin excludes it.


How other agents handle standing approvals

Claude Code's design — approval file at the repository root — is a deliberate architectural choice: approvals are project-local, so running the agent in two repositories on two different machines gives independent approval lists. The trade-off is that the file lands inside the folder being synced.

Other agents make a different trade-off.

Codex CLI (OpenAI) writes TUI allow-list additions to a user-layer file: ~/.codex/rules/default.rules.12 This path lives in the home directory, not inside any project folder, so it is not swept up by a folder-scoped sync client. The trade-off is that an approval granted in one project applies to all projects for that user.

Gemini CLI persists approvals to ~/.gemini/policies/auto-saved.toml by default, unless you open the agent with a workspace-scoped session.13 The workspace tier keeps policies inside the project; the user tier keeps them in the home directory. The user tier's path, like Codex's, is outside the project folder and therefore outside most sync scopes.

Neither design is strictly superior. A user-level approval list is not inside a vault, but it is not version-controlled either, and it accumulates across all projects. A project-level file is auditable and repo-specific, but it must be explicitly excluded from each sync client. Know which model your agent uses; the answer determines which exclusion work you need to do.


The safe pattern for each scenario

If you are using Claude Code:

First, verify that the auto-exclude took effect:

git check-ignore -v .claude/settings.local.json

If this prints a line naming your global excludes file, git is ignoring the file correctly. If it prints nothing, Claude Code has not yet written the file — add the exclusion yourself:

echo '**/.claude/settings.local.json' >> ~/.config/git/ignore

Then, audit what is actually in the file before you sync:

grep -rni --include=settings.local.json -E 'pass|token|key|secret|api' .

If this returns any hits, the commands that produced those rules are worth reviewing. The correct fix is not to scrub the file — it is to remove the secret from the approval. The team that builds Claude Code put it plainly: "Regardless of what agent you use, you should have secret scanning enabled."4

The architectural pattern that eliminates the problem entirely: keep secrets in environment variables or a password-manager CLI so the approved command string contains only a variable name, never the secret itself:

# Risky approval string (don't store this)
PGPASSWORD=hunter2 psql -U deploy -d production -f migrate.sql

# Safe approval string (store the variable reference, not the value)
psql -U deploy -d production -f migrate.sql
# with PGPASSWORD in ~/.pgpass or set in the environment before the session opens

If your vault is synced by a file-sync client:

For Syncthing, add .claude/settings.local.json to .stignore. For Dropbox, mark the .claude folder with the ignored attribute:

# macOS / Linux with Dropbox desktop client
xattr -w com.dropbox.ignored 1 .claude

For Obsidian Sync and remotely-save, files beginning with . are excluded from sync by default, which covers .claude/settings.local.json. Verify this in the plugin's excluded-files setting before trusting it.

The check that covers all cases:

After configuring your sync tool, open the service's web interface or a second device and verify that .claude/settings.local.json is not present. The file exists on disk locally — the question is whether it has been copied elsewhere.


Why this matters for note vaults specifically

The Obsidian plugin registry counts more than 6.3 million combined downloads for plugins that run AI agents — including Claude Code, Codex, and OpenCode — directly inside a vault.11 The same registry counts more than 6.8 million combined downloads for file-sync plugins.11

A vault is a folder. When you run an agent inside a vault, the agent's approval file lands in that folder. Where the vault's files go is whatever you configured — Syncthing, Dropbox, iCloud, a plugin — and those services copy what they find.

This is not a vulnerability in Obsidian or in any sync plugin. It is the natural consequence of running a project-scoped agent inside a folder that a sync client treats as a unit. The agent, the git layer, and the sync layer are three independent programs with three independent ignore systems. Their responsibilities do not overlap by default.

Post #126 in this series, ".gitignore for Your Note Vault", covers git's three ignore levels in detail — committed .gitignore, repo-local exclude, and global excludes — and notes that sync tools have their own analogous mechanisms. The agent-approval case is the specific scenario where git's global-excludes protection is in place and correct, yet a secret can still escape via a different path. The two posts together map the full terrain.


Frequently Asked Questions

What file does Claude Code write when I click "don't ask again"?

Claude Code writes .claude/settings.local.json at the root of the git repository you opened it in. The file contains a JSON object with an allow array. Each entry in that array is the exact string of the command you approved.1

Does clicking "don't ask again" expose my password?

Only if the approved command contained the password as a literal string. If you approved PGPASSWORD=hunter2 psql -U deploy, then yes, that string is in the file. If the command used an environment variable or a credential helper, the file contains only the variable reference, not the value. The safe pattern is to keep secrets in environment variables before you start the agent, so they never appear in the command string.2

Git already ignores .claude/settings.local.json. Isn't that enough?

For git, yes. Git will not commit the file or push it to any remote. But a file-sync client — Dropbox, iCloud, Syncthing, a vault-sync plugin — does not read git's ignore list. Each has its own ignore mechanism. If you have not explicitly excluded the file from your sync client, it will be copied.5

Which agents store approvals inside the project folder?

Claude Code stores .claude/settings.local.json at the repository root — inside the project folder, excluded from git but not from other sync.1 Codex CLI stores allow-list additions in ~/.codex/rules/default.rules — in the home directory, outside the project folder.12 Gemini CLI defaults to ~/.gemini/policies/auto-saved.toml, also in the home directory, unless you use a workspace-scoped session.13 The home-directory designs are outside most vault-scoped sync, but they accumulate across all projects.

How do I audit what is in my approval file?

Run grep -rni --include=settings.local.json -E 'pass|token|key|secret|api' . from the root of your vault. If you see hits, review which approved commands produced those entries and consider whether they contained secrets that should not persist.

How do I exclude the file from Dropbox specifically?

Use the per-item ignored attribute. On macOS or Linux with the Dropbox desktop client: xattr -w com.dropbox.ignored 1 /path/to/vault/.claude. Dropbox removes the folder from the server and from all other devices. There is no .dropboxignore file in Dropbox's model.7

Does Obsidian Sync copy .claude/settings.local.json?

Obsidian Sync excludes dot-prefixed folders except .obsidian. The .claude folder should therefore be excluded by default, but you should verify this in the sync settings panel and confirm the file is absent in the mobile client or vault web UI.9

What should I do if I have already synced the file with secrets in it?

Remove the secret from the approval rule (edit or delete the relevant entry in .claude/settings.local.json), then configure your sync client to exclude the file going forward, and trigger a sync to push the deletion or exclusion. If the secret was in the file and is now on another device or a cloud server, treat it as compromised and rotate it.


The operating system that matters for your vault is not macOS or Linux. It is the composition of every program that touches your folder: the agent that writes files there, the version-control daemon that tracks them, and the sync client that moves them. Each has its own ignore logic, and none of them consults the others.


Notes stored on your own device stay where you put them. mnmnote.com is a local-first markdown editor — your files, your sync policy, your call.

References


Footnotes

  1. "Settings," Claude Code documentation, Anthropic. https://code.claude.com/docs/en/settings.md (last modified 2026-11-07; accessed 2026-11-08). ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  2. Aurornis (HN username), comment on "Claude Code's 'Don't Ask Again' exposes passwords in 1,122 public GitHub files," Hacker News item 50408084, 2026-11-07. https://news.ycombinator.com/item?id=50408084 (accessed 2026-11-08). ↩ ↩2

  3. Apiiro Research, "Coding Agents Leaking Secrets on GitHub: The 'Don't Ask Again' Problem," apiiro.com/blog, published 2026-11-05 (data collected November 1–3, 2026). https://apiiro.com/blog/coding-agents-leaking-secrets-github-dont-ask-again (accessed 2026-11-08). ↩ ↩2 ↩3 ↩4

  4. bcherny (Anthropic), reply on Hacker News item 50408126, 2026-11-07, 21:13 UTC. https://news.ycombinator.com/item?id=50408126 (accessed 2026-11-08). ↩ ↩2

  5. "gitignore(5)," Git Reference Manual, git-scm.com. https://git-scm.com/docs/gitignore (accessed 2026-11-08). ↩ ↩2 ↩3

  6. "Ignoring Files," Syncthing documentation, docs.syncthing.net. https://docs.syncthing.net/users/ignoring.html (accessed 2026-11-08). Wayback: web.archive.org/web/20261108012505. ↩

  7. "Ignored files," Dropbox Help, help.dropbox.com. https://help.dropbox.com/sync/ignored-files (accessed 2026-11-08). Wayback: web.archive.org/web/20261108012526. ↩ ↩2 ↩3

  8. "Files that don't sync with Dropbox," Dropbox Help, help.dropbox.com. https://help.dropbox.com/sync/files-not-syncing (accessed 2026-11-08). Wayback: web.archive.org/web/20261108012554. ↩

  9. "Sync settings and selective syncing," Obsidian Help, obsidian-help repository @65112ae, en/Obsidian Sync/Sync settings and selective syncing.md (accessed 2026-11-08). ↩ ↩2

  10. remotely-save README (master branch); obsidian-livesync README (main branch) (accessed 2026-11-08). ↩

  11. Obsidian community-plugin-stats.json, obsidian-releases @20abb3e (accessed 2026-11-08). copilot: 3,340,702 downloads; realclaudian: 2,652,561; agent-client: 327,630; remotely-save: 2,364,070; obsidian-livesync: 1,094,051; obsidian-git: 3,422,527. ↩ ↩2 ↩3

  12. "Rules," Codex CLI documentation, learn.chatgpt.com. https://learn.chatgpt.com/codex/rules (last modified 2026-11-07; accessed 2026-11-08). ↩ ↩2

  13. Gemini CLI source, packages/core/src/policy/config.ts and config/storage.ts, commit @62bab5b; docs/reference/policy-engine.md (accessed 2026-11-08). ↩ ↩2