Stop embedding .env files; stop returning document text three times - #6
Open
seiz888 wants to merge 3 commits into
Open
Stop embedding .env files; stop returning document text three times#6seiz888 wants to merge 3 commits into
seiz888 wants to merge 3 commits into
Conversation
`.env` sits in the isIndexable() extension allowlist, so rag_index_project reads any `.env` (and any `*.env`) in a scanned project and ships its contents to the configured embedding provider — Voyage AI by default — where it then persists in the vector store, semantically retrievable by any agent with access to the collection. This reads as an oversight rather than a decision: `.env.local`, `.env.production` and `.env.example` are all skipped already, because filepath.Ext() returns `.local`/`.production`/`.example` for them. Only the bare `.env` and `*.env` forms match the allowlist. The walker also indexes `.gitignore` as a document but never consults it, so a `.env` excluded from version control precisely because it holds credentials is still read. Drop `.env` from the allowlist, and add isSensitive() as a second gate for names that are otherwise indexable: `secrets.json`, `db_password.yml` and `credentials.yaml` all reach the embedder today through the allowlisted `.json`/`.yaml`/`.yml` extensions. The name-substring match is deliberately broad — skipping a document that merely mentions a password is a cheaper mistake than embedding a live credential. For the record, key material (`id_rsa`, `*.pem`, `*.key`, `.netrc`, `.pgpass`) was NOT affected: those extensions are absent from codeExts, so they were already skipped. isSensitive() now covers them too, so the guarantee no longer depends on codeExts never growing. Co-Authored-By: Claude Mythos <noreply@anthropic.com>
The Qdrant read path copies every string payload field into Result.Meta, and "content" is a payload field — so each search result carried the full document text in both .Content and .Meta["content"]. rag_retrieve_context then returned the concatenated "context" string alongside those chunks, putting the same text into a single tool response three times. Agents pay for all three in tokens; two of them are unreadable noise. - exclude the content key from the metadata map on read (search + export) - return chunks from rag_retrieve_context as references (id, score, meta); the context string already carries the text with its scores While in the read path, surface the remaining payload tags through a new PointInfo.Meta and let rag_list_points take an exact-match metadata filter. Both are needed when the store holds agent memory tagged with bucket/kind/ts rather than a code index — previously only source_file was filterable, and those tags were not returned at all. Co-Authored-By: Claude Mythos <noreply@anthropic.com>
Flags the rag_retrieve_context response-shape change as breaking under [Unreleased] > Changed, and files the .env leak under Security. Co-Authored-By: Claude Mythos <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
1.
.envfiles are read and sent to the embedding provider.envis present in theisIndexable()extension allowlist, sorag_index_projectreads any
.env(and any*.env) in a scanned project, sends its contents to theconfigured embedding provider — Voyage AI by default — and persists it in the vector
store, where it stays semantically retrievable by any agent with access to the
collection.
This is the documented happy path, not an edge case: the README tells agents to call
rag_index_projectto sync file changes, and a typical Node/Next/Django project keepsa
.envat its root.It looks like an oversight rather than a decision, because the handling is inconsistent:
filepath.Ext().env.envprod.env.env.env.local.local.env.production.production.env.example.exampleNobody deliberately designs "index
.envbut skip.env.production". The walker alsoindexes
.gitignoreas a document but never consults it, so a.envthat is excludedfrom version control precisely because it holds credentials is still read.
Fix: drop
.envfrom the allowlist and add anisSensitive()gate afterisIndexable().Scope, stated honestly
This is not remotely exploitable — it needs an operator to index a directory containing
a
.env. The impact is that the operator's own credentials leave their machine to athird-party API and persist in their own vector store. Moderate severity, high
likelihood, two-line root cause.
Key material was never affected.
id_rsa,*.pem,*.key,.netrcand.pgpasshave extensions absent from
codeExts, soisIndexable()already rejected them; Ichecked rather than assumed.
isSensitive()now covers them anyway so the guarantee nolonger depends on
codeExtsnever growing.What the second gate does newly protect are names that are otherwise perfectly
indexable through allowlisted extensions:
secrets.json,db_password.yml,credentials.yaml. The name-substring match is deliberately broad — skipping a documentthat merely mentions a password is a cheaper mistake than embedding a live credential.
2. Retrieval returns the same text three times
The Qdrant read path copies every string payload field into
Result.Meta, andcontentis a payload field. So each result carried the document text in both
.Contentand.Meta["content"].rag_retrieve_contextthen returned the concatenatedcontextstring alongside those chunks — putting the same text into one tool response three
times. Agents pay for all three in tokens; two are unreadable noise.
chunksfromrag_retrieve_contextas references (id,score,meta)Warning
Breaking:
rag_retrieve_contextno longer returnschunks[].Content. Thecontextstring already carries every chunk's text with its score, so callers shouldread it from there. Flagged in the changelog. The repo is pre-1.0, so this seemed
preferable to shipping an opt-in flag that would leave the default path paying triple.
3. Metadata filtering for memory-shaped collections
While in the read path:
PointInfonow carries the remaining payload tags inmeta,and
rag_list_pointsaccepts an exact-matchfilteron any metadata key rather thanonly
source_file. Both are needed when a collection holds agent memory tagged withbucket/kind/tsinstead of a code index — previously those tags could neither befiltered on nor read back.
Tests
Five new tests. Each was mutation-checked: reverted the fix, confirmed the test fails,
restored it, confirmed it passes.
TestIsSensitive— the predicate, including the deliberate over-inclusionTestIndexerSkipsSensitiveFiles— writes secrets to disk, asserts they never reach the providerTestQdrantSearchMetaExcludesContent— text returned once, useful tags surviveTestQdrantListPointsSurfacesMeta—bucket/kind/tsexposed, promoted fields not echoedgo build ./...andgo vet ./...are clean.pkg/ragandpkg/indexerpass.pkg/httpapihas 14 failures on Windows — verified pre-existing by running the samesuite on a pristine clone of
main: identical 14, so none are introduced here. They arePOSIX-permission and HOME-path assumptions (
expected 0600 ... got 666).🤖 Generated with Claude Code