Auto Context — auto-inject files like Claude.ai Projects (part 2)
The second half of the Auto Context page: flagging a doc as an authoritative project instruction, letting an external agent add or remove files through CLI Control with an inbox approval, and the machinery behind the injection — the builders, the per-engine first-message paths, the budget and the kill switch.
What it is
This is part 2 of the Auto Context (.claude/docs/ auto-injection)
page. That page covers what Auto Context is, where the sidebar group and the .claude/docs/
folder live, and how files are added, ordered and counted on the way into a session. This half
carries the rest: the flag that promotes a doc from background reference into a binding
instruction, the CLI Control surface an outside tool uses to change the folder, and the
machinery that builds the injection in the first place.
Where to find it
Two surfaces here, both reached outside the Auto Context panel itself. Inject as instruction is a right-click action on a doc row inside the Auto Context group. CLI Control is Omniscio's local HTTP server, which an external tool — another Claude Code window, a script, a macro — calls to add or remove files, with your approval in the inbox as the gate.
How it behaves
Inject a doc as an authoritative instruction
The always and rag buckets both deliver a doc as reference material — context the agent may consult. Sometimes you instead want a doc to be an instruction the agent must follow — a house style, an "always do X" rule, a workflow. For that, flag the doc as an instruction.
How. Right-click any markdown/text doc in the Auto Context panel → Inject as instruction (toggle it back with Stop injecting as instruction). Flagged docs show a small Instruction badge on their row.
What changes. A flagged doc is pulled OUT of the first-message ## Injected Project Docs block (the background context the agent can skim past) and delivered instead as a ## Project Instructions section of the session's system prompt — the standing instructions the agent carries on every turn, not one-time background context. This is the per-project equivalent of Settings → Agent Instructions → Custom Instructions (which is global); flagging a doc scopes that same authority to one project. It reaches every engine, not just Claude.
Good to know:
- A flagged doc claims its share of the injection budget first, ahead of the background docs — so a large instruction doc can be the reason a background doc is skipped. That is deliberate: you elevated it, so it wins the budget. (The two share one budget rather than getting one each, which is what keeps a session's first message inside the context window its engine actually enforces.)
- If a flagged doc is so large it does not fit the budget on its own, its body is withheld rather than cut in half — the agent is told the instruction exists, that it is binding, and given the full path to read it. A half-delivered rule set is worse than a missing one, so it is never truncated. The doc's row shows the same skipped warning a background doc gets; if you see it, unflag a doc or move the project to a larger-window engine.
- Like all doc injection, it takes effect on the next session's first message — not on sessions already running.
- Rename a flagged doc and the flag follows the new name; delete it and the flag cleans itself up.
- Only a top-level text/markdown doc is eligible — a
rag/doc or a binary can't be an instruction. The body is injected verbatim and unfenced (so the agent reads it as instruction, not data), so keep instruction docs concise and directive.
Under the hood the flag is a per-project list of filenames (projects.doc_instruction_json, toggled via the files:set-project-doc-instruction IPC); the content is assembled by buildProjectInstructionsContent and delivered as the engine system-prompt bundle's project-instructions fragment. Invariants (exclusion from the always-bucket, first claim on the shared budget, authoritative routing): project-docs-injection-contract.md
(instruction-doc-excluded-from-always-bucket-but-NOT-from-the-budget).
Adding/removing files from an external agent (CLI Control)
If you want an external AI tool — Claude Code in another window, ChatGPT via a script, an AutoHotKey macro — to add or remove files in a project's .claude/docs/ without you opening Explorer, Omniscio's CLI Control server (see cli-control.md) exposes three endpoints under /project/:id/docs:
GET /project/:id/docs— list the current.claude/docs/contents (read-only, auth-gated). Response splits files intofiles(thealwaysbucket) andragFiles(theragbucket) with separatetotalBytes/ragTotalBytestotals so external agents can tell which bucket each file lives in without re-classifying paths.POST /project/:id/docs— enqueue an upload that Omniscio copies in after you approve it in the inbox. The request body accepts an optionalbucket: 'always' | 'rag'field (default'always'for backward compatibility);'rag'routes the file into therag/sub-folder and skips the 100 MB per-project quota check.DELETE /project/:id/docs/:filename— enqueue a delete that runs after you approve it in the inbox. Also accepts thebucketquery parameter to disambiguate when a filename exists in both buckets (e.g.?bucket=rag).
Both mutations land as pending rows in the same cli_pending_actions queue that gates settings PATCH and session lifecycle — they appear in Omniscio's inbox as approve/reject cards and have no side effect until you click Approve. That's the user-in-the-loop guarantee: an external agent cannot silently rewrite your knowledge folder. The pending-action card shows which bucket the file targets so you can reject an always-bucket upload that should have been rag (or vice versa) before it lands. See cli-control.md for the full request/response shapes (filename rules, 25 MB per-file cap, 100 MB per-project quota — always bucket only, idempotency header, error codes).
The path-based upload contract — agent writes a temp file, passes its absolute path, Omniscio does the copy — keeps the request body constant-size regardless of file size and dodges base64 bloat.
For agents
How it works
Everything happens in one place — the moment Omniscio builds the agent's first user message — and it's all done by Omniscio in TypeScript. There is no shell hook, no <system-reminder>, no CLAUDE.md write. The docs builder is buildProjectDocsContext(projectFolderPath, projectId), which returns the inline block (text content + RAG TOC) or null when there's nothing to inject.
Every engine, the same builders. buildProjectDocsContext is wrapped by buildSessionDocsContext in Omniscio's single-source-of-truth session-auto-context module — the same builder the Claude spawn path, the crash-recovery replay, AND every external engine's first message build on. The external engines compose the docs + your AI-coaching profile via buildFirstMessageAutoContext. So the docs reach not just Claude but Codex, Gemini, Cursor, Pi, OpenClaw, OpenCode, and Hermes too — and any external engine added later inherits it for free (a build-failing guard forbids one from composing a first message without it). Full mechanics + per-engine invariants: launch-auto-context-contract.md.
Where it's wired in. A session's first message reaches its engine through one of these code paths, each prepending the docs block:
- Claude, with an initial prompt (Super Prompt, automation, recipe, cron-heal, coaching, deep link). In session-create.ts the block is prepended to
launchPromptbefore the process launches. - Claude, without an initial prompt (you open a blank session and type the first message). In session-service.ts's
sendResponse, gated on the first message only. - External engine, at launch (CLI
/project/:id/new, deep link, Quick Launch, automation). In launch.ts'slaunchGatedNonClaudeProvider, prepended to the SENT text. - External engine, at first send — Codex / Gemini / Cursor / Pi / OpenClaw / OpenCode / Hermes. Each engine's branch in session-service.ts prepends the block via the shared
externalFirstMessageAutoContexthelper, gated on the first operator message only.
All paths skip virtual projects (Omniscio's own UI agents carry purpose-built context, not user docs) and inject in exactly one place per first message to avoid double-injection. The builders wrap each phase in try/catch so a docs-enumeration failure can never block the spawn or the send — worst case the prompt goes out without the block and the agent can still Read the files.
Invisible on every engine. The block is prepended to what is SENT to the engine only — never the persisted operator row. On Claude that's launchPrompt / the built prompt (session-create.ts persists the raw input.initialPrompt; sendResponse persists the typed displayText). On external engines the launch path passes the RAW user text as a separate displayText argument, so an engine that persists the operator row at launch (gemini-ACP, the one-shot family) writes only what the user typed — never the docs block. So the chat bubble, the database, search, export, and share all show exactly what the user wrote, on every engine.
The always bucket — text inlined. buildProjectDocsContext enumerates the top level of .claude/docs/ (skipping the rag/ sub-folder and symlinks), filters to text-classified extensions via TEXT_EXTENSIONS from project-docs.ts, orders them with applyDocOrder (same sidebar order as everywhere else), and emits:
## Injected Project Docs
### style-guide.md
` ` `
…full file contents…
` ` `
### glossary.md
` ` `
…full file contents…
` ` `
There is no per-file cap — any single text file is inlined in full. The project-wide budget is docInjectionBudgetBytes(provider, model) in project-docs-caps.ts: a DOC_SHARE_OF_WINDOW (25%) share of enforcedContextWindowTokens — the window the claude binary actually holds that engine to, which is NOT always the window the vendor publishes — clamped by MAX_TOTAL_BYTES / MAX_TOTAL_TOKENS (500 KB / 125K tokens). Those two constants are the ceiling, not the allowance: raising them only ever loosens the 1M-window lane, and can never hand a ~200K-window engine more than its share. A file that would push the running total over the budget is replaced by a one-line *Skipped — would push total over NNK token cap. Use Read tool on \<absolute path>` when needed.*note carrying the engine's real number, and the loop **keeps going** so a smaller file later in the order can still fit. That path is absolute on purpose: the docs live in the **project** folder while the agent's working directory is the session's worktree, so a relative pointer would resolve against the wrong directory and theReadwould fail. Docs flagged "inject as instruction" are subtracted from this same budget before the background bucket sees it, so the two blocks together can never exceed one share. The same budget feeds the sidebar's **skipped** counter, the always-inject portion of its "Injected" total, and — throughbudgetTokensK`, the single bytes-to-thousands conversion the skip tooltip also calls — the cap figure its two summary lines name, so the docs-bucket accounting in the UI and the actual injection never disagree. (The headline "Injected" number additionally folds in the natively-loaded System Instructions — see the Auto Context group description above — but this budget governs only these always-inject docs.)
The rag bucket — table of contents. buildProjectDocsContext then enumerates .claude/docs/rag/ (alphabetical) and appends a catalog under its own heading:
## Available Reference Docs (RAG — fetch with Read tool)
- .claude/docs/rag/schema.sql (~3K tokens) — "Canonical DB schema"
- .claude/docs/rag/full-api-spec.md (~61K tokens) — "Public API reference"
- .claude/docs/rag/archive-2024.pdf (8.2 MB)
- … and 4 more
Text entries carry a token estimate plus a one-line description (the first markdown # H1, or the first non-empty line, truncated to 120 chars); binary entries carry a human-readable size only. The TOC is capped at 100 entries with a - … and N more overflow line so its token cost stays bounded regardless of how many files live in rag/. The bytes of the RAG files themselves are never inlined and never count against the always bucket's injection budget — only the ~one-line-per-file catalog does. The bucket is skipped entirely (no empty heading) when rag/ is missing or empty; a project that uses ONLY the RAG bucket still gets its catalog because each bucket is built independently and the two non-empty blocks are joined.
The always bucket — binaries attached. Binary handling is unchanged and lives in session-service.ts's first-message path, separate from buildProjectDocsContext. On the first message it resolves the session's projectId → Project.folderPath → resolveProjectWorkDir(folderPath) → <workdir>/.claude/docs/, then calls getProjectDocsFiles() to list files whose extension is in ATTACHABLE_EXTENSIONS (images, PDFs, .docx/.xlsx/.pptx). The enumerator deliberately skips the rag/ sub-folder — RAG-bucket binaries are catalog-only, never auto-attached. The resulting absolute paths are passed to buildPromptWithAttachments() in image-store.ts, which prepends a "Please examine the following file(s): …" header plus the paths to the prompt. From the CLI's perspective this is indistinguishable from the user having clicked the attach button, so PDFs and images go through its native multimodal ingestion pipeline. Virtual projects work because resolveProjectWorkDir() in claude-project.ts maps sentinels like __claude__ → ~/Claude before the lookup; virtual projects with no real workdir throw on readdirSync, which getProjectDocsFiles() catches and returns [] — a correct no-op.
Why text-inline-as-content rather than attaching the text files too. Text is cheap to drop straight into the message body and reads naturally as context the model already has — no extra Read round-trip, no multimodal pipeline. Binaries can't be inlined as text (the markdown body can't carry a PDF or an image), so they need real message attachments: buildPromptWithAttachments() builds a user message whose paths the CLI's native multimodal pipeline opens. The user explicitly rejected "tell the model to please read the PDF" and demanded "force-attach it like the user clicked attach" — so binaries route through the attachment path, not a "here are some filenames" note. Full design record and rejected approaches in the auto-injection postmortem.
Kill switch. AMC_DISABLE_DOCS_INJECTION=1 (strict === '1') makes buildProjectDocsContext return null — disabling the inline text + RAG TOC path only. Binary attachment via getProjectDocsFiles() is a separate code path and is not affected by this switch.
Why the RAG bucket has no separate retrieval system. Omniscio deliberately does NOT ship an embedding store, vector DB, or selector-based retrieval. The agent already has a Read tool, the agent already sees the file catalog in the first message's TOC, and the agent is competent at picking relevant files from a labeled list. Adding an embedding layer would be a separate moving part to maintain, a separate index to keep coherent with disk, and a separate failure mode (semantic mismatch) — for no win over "list the filenames and let the agent decide". If a project's RAG bucket grows past ~50 files and the agent starts struggling to pick from the catalog, the right answer is to organize the bucket (rename for clarity, split into multiple projects, or move stale files out) rather than to layer retrieval on top.
Related
Auto Context (.claude/docs/ auto-injection) is the first half
of this page — the buckets, the sidebar group, ordering, and the first-message chip. The
contracts and source files named under For agents above hold the invariants behind everything
this half describes.
Last verified 2026-09-23