What agent memory tools capture, and how to keep secrets out
Memory tools change the secret-handling question. A chat that forgets is a chat you can afford to paste a key into, once, badly. A memory tool keeps the saved conversation, the tool output, or the note. The key is still there next week.
There are two ways memory gets written. An automatic tool records what happened: prompts, tool calls, tool results, session boundaries. A deliberate store records what an agent chooses to write. The first captures more. The second is easier to review. Neither is a guarantee.
Most capture starts with a hook, a command the harness runs at a lifecycle event such as session start or after a tool call. Claude Code's auto memory is a different thing: notes the model writes for itself about a project, not text you write.
Checked against each tool's documentation on 26–27 October 2026.
Two capture models#
| Model | How it works | Tools that use it | What to worry about |
|---|---|---|---|
| Automatic | Hooks or a background pass record the session, then compile it into memory. | ai-memory lifecycle hooks; Claude Code auto memory; Codex memories | The prompt or tool output contained a secret before anyone decided to remember it. |
| Deliberate | An agent calls a write tool, or you edit a file. | A note store; CLAUDE.md; AGENTS.md | The agent writes the secret on purpose, or copies it from a file it read. |
| Mixed | The model decides what is worth saving, then writes it. | Claude Code auto memory; Codex memories; Copilot Memory | The model's judgement is the filter, and it is not a secret scanner. |
What each tool captures#
ai-memory#
ai-memory is the most capture-heavy tool here. Its lifecycle hooks record prompts, tool calls and results, and session boundaries. In our test of version 3.2.5, a six-event Claude Code-shaped session produced six stored observations and a session page. The prompt and tool body were stored, including the synthetic secret-shaped strings we planted.
ai-memory runs the text through a sanitizer before storage: a pattern matcher that replaces or removes known secret shapes. In our test it replaced a synthetic GitHub token, a password=... shape, a ~/.ssh path, an sk-ant-... key and a postgres://user:pass@ URL. It also over-redacted: an API_KEY=... match consumed the adjacent CACHE_WARM label, so the useful diagnostic became [REDACTED:env_secret] was 0.
The default install is local. The data handling docs say there is no telemetry and no encryption at rest. One default network call is the ~87 MB local embedding model download from Hugging Face on first start (local embeddings). An embedding model turns text into vectors so search can compare meaning; this one runs on your machine, and the download carries no memory content.
Consolidation is the model-assisted rewrite of stored notes into pages; lint checks them for problems. A webhook is an HTTP callback to a URL you configure. Several opt-in paths do send content out:
- A cloud embedding provider sends the text of the pages it embeds. Switching from local to cloud can backfill the existing corpus.
- An LLM provider — a hosted or self-hosted language model — sends session observations and page text on consolidation, lint, bootstrap, dream, profile and auto-improve runs (LLM providers).
- An LLM reranker sends the query plus bounded titles and snippets.
- Admission webhooks send every write's path, its metadata block and its body to the URL you configured (admission webhooks); a webhook is an HTTP callback to that URL.
The marker file docs also describe [capture] ignore_paths as a best-effort filter, not a guarantee. It matches explicit file-tool paths, but a shell command is read lexically and an unknown tool is not read at all. For a sensitive repository, the docs recommend allowlist capture mode and no marker: nothing from an unmarked repository is captured at all.
Claude Code#
Claude Code has two memory surfaces. CLAUDE.md is text you write. Auto memory is text Claude writes. The memory docs say auto memory is on by default in local sessions, stored as plain markdown under ~/.claude/projects/<project>/memory/, and loaded at the start of every session. MEMORY.md is the index; topic files are read on demand with normal file tools. Claude decides what is worth saving and skips anything it can derive from the codebase or that CLAUDE.md already says.
That means a secret Claude saw in a file or a tool result can become a memory file if Claude judges it useful. The docs do not describe a secret scan on auto memory writes. They do say the memory files are excluded from the transcript cleanup and stay until you or Claude edits or deletes them.
The local transcript — the conversation log Claude Code saves under ~/.claude/projects/ — is a second copy. Claude Code stores it in plaintext for 30 days by default, adjustable with cleanupPeriodDays (data usage). Prompts and model outputs also go to Anthropic over TLS, the encryption behind HTTPS, for the model request; retention depends on your plan and provider.
You can reduce what Claude reads:
- A
Readdeny rule such asRead(./.env)blocks the built-in file tools, and it also blocksEditandWriteon that path. - Read deny rules do not cover arbitrary subprocesses. A Python script that opens a file itself is outside that check.
- The sandbox is the OS-level boundary for shell commands. It is off by default, and its default read access covers most of the machine, including credential files such as
~/.sshand~/.aws/credentials. - To protect credentials, list them under
sandbox.credentialswithmode: "deny", or usefilesystem.denyRead. The sandbox covers shell commands only; the Read tool, MCP servers and hooks run outside it. MCP is the protocol an agent uses to call an outside tool.
None of these settings scans auto memory after a write. If the goal is "no secret in memory", the reliable control is to keep the secret out of the session or turn auto memory off for that project.
Codex#
Codex memories are off by default. The memories page describes a local store under the Codex home (by default ~/.codex/memories/), generated from eligible prior chats in the background. It says Codex redacts secrets from generated memory fields, and it tells you not to store secrets in memories and to review the files before sharing your Codex home directory.
The redaction claim is narrower than it sounds. It covers generated memory fields, not every copy in the store. The store holds summaries, durable entries, recent inputs and supporting evidence drawn from prior chats. Scan the whole directory, not just the summary. The same Codex home also holds history.jsonl unless history persistence is off, plus logs and caches. Before you share a Codex home, grep it for a canary token you planted.
The import path is another copy. The import page sends Claude Code project memories to Codex memories. If an old Claude Code project memory held a secret, the import gives it another place to land. Turning memories off stops new generation; the desktop app's Delete Codex memories is the documented reset.
Two controls matter:
memories.disable_on_external_contextkeeps chats that used MCP tools, web search or tool search out of memory generation.memories.use_memoriesandmemories.generate_memoriescontrol whether Codex injects existing memories and whether a chat can become a generation input.
The shared rule file is a separate risk. AGENTS.md is committed, so a key pasted into it is in the repository history. Keep secrets out of both the global AGENTS.md under CODEX_HOME (default ~/.codex/AGENTS.md) and the project file.
Codex also has a shell environment policy. The advanced configuration docs say ignore_default_excludes defaults to true, so Codex does not automatically remove variables whose names contain KEY, SECRET or TOKEN. Set it to false and add filters, or use inherit = "none" and pass only what the command needs. Local history is a separate file, $CODEX_HOME/history.jsonl (default ~/.codex/history.jsonl); [history] persistence = "none" turns it off.
Gemini CLI#
Run gemini --version first; this section describes v0.65.0 (20 October 2026). Gemini CLI has no save_memory tool in this version. When the agent saves a fact, it edits a Markdown file with write_file or replace. That makes the write easy to inspect, and it also puts the text in a file the CLI loads into every prompt.
The routing rule is the risk. The system prompt sends a team convention to GEMINI.md in the repository, a private note to the private project folder's MEMORY.md, and a cross-project preference to ~/.gemini/GEMINI.md. If the model decides a secret is a project convention, it can write it into GEMINI.md, a file meant to be committed. If it decides it is a personal preference, the secret goes into the global file, where every project on the machine loads it. Either way it is in the prompt on every turn, and in git history if the repository file was committed and pushed.
A private note is quieter but not safe. MEMORY.md and its sibling notes are plain Markdown under your home directory; the CLI does not encrypt them, and a sibling note is still on disk after you remove its pointer.
Auto Memory adds a second path. It is off by default. When experimental.autoMemory is on, it reads old session transcripts and sends excerpts to the configured model to draft patches. The docs say the extraction agent is instructed to redact secrets and tokens, and that is best effort, not a guarantee. Candidates sit in the project memory folder's .inbox until you apply them; an unapplied candidate has not changed a loaded file, but the transcript it came from is still on disk.
The docs for direct memory writes describe no secret scan. The core prompt's "never log, print, or commit secrets" is an instruction to the model, not a filter on the file.
Check the copies:
git status --short
git diff -- GEMINI.md
grep -RE "sk-ant-|ghp_|password=" ~/.gemini/GEMINI.md ~/.gemini/tmp/*/memory/ 2>/dev/null
find ~/.gemini/tmp -path '*/.inbox/*' -type fThe file tiers and what each one holds are in what the memory command shows in Gemini CLI.
Qwen Code#
Qwen Code's private memory has no documented secret scan. It has two auto-memory tiers, both plain Markdown and both model-written: the project tier at ~/.qwen/projects/<project>/memory/ and the user tier at ~/.qwen/memories/. Each has a MEMORY.md index and topic files. Version 0.26.2 (27 October 2026) is the reference. The memory docs describe what Qwen looks for — facts about you, feedback, project context, links — and name no secret-scan step for those writes. Treat a secret Qwen saw in a file or a tool result as a possible memory entry.
The team tier is different. .qwen/team-memory/ is committed to the repository, so a secret there is in git history and visible to everyone with repository access. Qwen scans team-memory writes for credentials such as API keys, tokens and private keys, and rejects a file on a match (team memory secret guard). The docs call that scan a backstop, not a guarantee; do not use the team tier for anything sensitive.
The instruction files load on every turn. QWEN.md and AGENTS.md can pull more text in with an @path/to/file import, so an import can put another file in the prompt and in a later memory write. Keep secrets out of QWEN.md, AGENTS.md and .qwen/QWEN.local.md; use an environment variable name.
/remember is a prompt, not a local write. The text goes to the model, which then writes a memory file, so a secret typed into /remember is already in the provider request. /forget asks a model to pick matching entries, and it can delete a whole file that has CRLF line endings (#14642, open, 19 October 2026). Back up the folder before a forget.
Qwen also keeps session transcripts as JSONL under ~/.qwen/projects/<project>/chats/. They are a second plaintext copy, like the memory files themselves.
Two controls: memory.enableManagedAutoMemory: false in .qwen/settings.json turns the private auto-memory off for that project, and the /memory panel has the same switch. Team memory is off by default (memory.enableTeamMemory). Scan both private folders and the team folder for a canary:
grep -RE "sk-ant-|ghp_|password=" ~/.qwen/projects/*/memory/ ~/.qwen/memories/ 2>/dev/null
git grep -n "sk-ant-\|ghp_\|password=" -- .qwen/team-memory 2>/dev/nullGitHub Copilot CLI#
Copilot Memory is a mixed case: a model decides what to save, but it saves to GitHub's server, not to a file you can inspect. Repository facts are shared with everyone who can use Copilot Memory in that repository; user preferences stay with the user. A secret written as a repository fact is a secret shared with the team.
The docs say repository facts are stored with citations pointing to code, and user preferences with citations that may include direct user quotes (About Copilot Memory). That matters more than the fact text: a pasted key, a private URL or a customer name can end up in a citation even if the summary line looks harmless. The Copilot Memory documentation describes citation validation, not secret scanning or redaction. Treat a memory write as unscanned.
You get some warning at write time. By default the CLI asks for permission before storing a memory, and since 1.0.49 the prompt names the scope: user, or the specific owner/repo (changelog). Read that line before you approve. If you save a memory approval in permissions-config.json, you approve memory writes and votes for that location without another prompt (configuration reference). That is convenient, and it is the wrong default for a repository that handles secrets.
Two controls are easy to miss:
- Prompt mode is off by default.
copilot -pdoes not use memory unless you pass--enable-memoryto turn it on. Leave it off in CI and other unattended runs; a run with it on can add facts to the same repository store if the account has write access. - Turning memory off does not delete.
/memory offstops the CLI using memory; the stored facts and preferences remain on GitHub until someone deletes them. Clearing~/.copilotdoes not remove them.
Status, 27 October 2026: on builds before 1.0.78, a report said background memory voting kept running while /memory show reported disabled (#3859, closed as fixed in 1.0.78). Run copilot --version before you rely on the off switch as a boundary; the current release is 1.0.104 (27 October 2026).
For the private-repo checklist, add Copilot Memory to the inventory: check repository Settings → Copilot → Memory and your personal Copilot settings → Memory, not a local folder. Turn it off for your account while you work in a repository that must not accumulate facts, avoid the blanket memory approval, and revoke a secret first if one was stored. Deletion is manual and does not reach a copy that another user may have already seen.
OpenCode#
OpenCode does not document a built-in memory store, so there is no background store to scan. The capture surfaces are narrower: the session database keeps the conversation, and AGENTS.md and the files listed in the instructions array are files you or an agent can write. The session database is local; V2's opencode session export (V1: opencode export) writes a session as JSON, and that export is the session data, so treat it like a transcript. V2's opencode session export --sanitize (V1: opencode export --sanitize) redacts sensitive transcript and file data as it writes; treat that as best-effort redaction, not a cleaned store. The docs checked describe no secret scan for the stored session or for instruction files.
The usual rule applies: keep secrets out of AGENTS.md, the files listed in instructions, and handoff files; use an environment variable name, not the value. If a secret reached a session, revoke it and delete the session (opencode session delete <id> in both lines). Deleting the session does not delete an export or a backup you made.
Sanitizers are best effort#
Every tool here has a detector list, a pattern matcher, or a model judgement. None is a security boundary.
The failure modes are ordinary:
- A format the list does not know.
- A secret split across lines, wrapped in quotes, base64-encoded, or written in a comment.
- A secret in a screenshot, a test fixture, a
.env.example, or a command's error output. - An over-redaction that removes the context you needed, so you cannot tell what failed.
- A revision, backup, or transcript that still has the original text after the current note was cleaned.
A scanner reduces accidents. Treat it as one layer, not the layer.
A private-repo checklist#
- Inventory every capture path. Claude Code auto memory and transcripts, Codex memories and history, Gemini CLI's global file, per-project memory and Auto Memory inbox, Qwen Code's instruction files, private auto-memory folders, team memory and session transcripts, Copilot Memory's repository and user pages, OpenCode session database and exports, ai-memory hooks and data directory, the note store you use, shell history, MCP server logs.
- Choose the capture mode per repository. ai-memory allowlist mode for a sensitive repo: only repositories you mark are captured. Claude Code auto memory off for that project. Codex memories off if you do not need them. Gemini CLI
experimental.autoMemoryoff if you do not want background extraction; direct file writes are not scanned. Qwen Codememory.enableManagedAutoMemory: falsefor that project, and leave team memory off unless the repository is meant to share facts. Copilot Memory has no per-repository switch: turn it off for your account while you work in that repository. If you use a note store, do not write the secret. - Keep secrets out of files that load every session.
CLAUDE.md,AGENTS.md,GEMINI.md,QWEN.md,.qwen/QWEN.local.md,MEMORY.md,.qwen/team-memory/,.ai-memory.tomland MCP configs. Use an environment variable name, not the value. - Deny reads where the tool supports it. Claude Code
Read(./.env),sandbox.credentialsfor~/.sshand~/.aws, andfilesystem.denyReadfor a wider block. Codexshell_environment_policyfilters. ai-memoryignore_pathsor allowlist mode. - Turn off outbound calls you do not need. ai-memory: no
llm_provider,embedding_provider = "none", no admission webhooks, no LLM reranker. Codex: memories off, ordisable_on_external_context. Claude Code: auto memory off for the project, and check what your MCP servers send. - Scan the stored copies, not just the note. Search the memory directory, the Codex memory store's supporting evidence and
history.jsonl, the Gemini CLI global file, per-project memory and.inbox, Qwen Code's private memory folders,.qwen/team-memoryandchats/, the OpenCode session database and exports, the transcript directory, the ai-memory data directory, and the note store's history for a canary token you planted — a fake secret whose only job is to show up if a scanner missed it. Read the GitHub Copilot Memory pages. Include backups and old revisions. - Separate by trust boundary. A separate ai-memory server, Codex
CODEX_HOME, Claude Code config directory, or note workspace per client. Do not rely on a path filter for a different client's data. - Rotate on find. A secret that reached a memory file, a transcript, or a note is a secret that may have reached a backup. Revoke it; do not just delete the line.
- Test the rule. Plant a fake key, run one real session, and search for it. A rule you did not test is a hope.
- Write the rule down. Put "do not write secrets to memory" and the allowed capture paths in the repository instruction file, where the next agent reads them.
Where Noteheld fits#
Noteheld is the deliberate contrast. It has no hooks, so a prompt or tool result is not recorded unless an agent writes a note. Writes are scanned, but the list is finite: in our test of the demo build, a Stripe-shaped key was redacted while a GitHub-shaped token, an AWS key and a password value were stored. Notes are plaintext on the server, and version 1 does not encrypt note bodies end to end. That makes it the better fit when the risk is a transcript you never review, and a poor fit when you need automatic capture.