Three commands, and no task to type.
You need Node 20 or newer, Docker running, and a TypeScript or JavaScript repository. Ichor is at its best on Next.js with Prisma, because it can then see your routes and your tables as well as your code.
npm i -g ichor-cli ichor init # installs the agent hooks, registers the tools, writes the stack ichor up # starts HydraDB and MinIO in Docker, bound to 127.0.0.1 ichor watch # reads your repo, then follows the conversation
ichor init writes into your repo and merges rather than overwrites — an existing .claude/settings.json, .codex/hooks.json or .mcp.json keeps everything it already had. It also adds .ichor/ to your .gitignore, because that is where Ichor keeps its own state.
The first read happens once: about 19 seconds for 1,362 files, and 2m 21s for the largest repository tested (7,735 files). After that, catching up on your edits takes seconds.
There is no command to describe the job.
Open Claude Code or Codex in the repo and work normally. Ichor reads the boundary from the prompt you were going to type anyway.
you > fix the duplicate email crash in vendor onboarding
[ichor] task set — 14 functions, data: Vendor
…hours later, same conversation…
you > now fix the rounding on billing invoices
[ichor] new task — 3 functions, data: Invoice
That second line is the part that matters. Nobody runs a CLI command between tasks, so a boundary set at 9am would still be policing vendor code at 2pm while you are deep in billing — questioning every edit until you switch the thing off.
Ask a question and nothing moves. "Where is link expiry enforced?" changes no boundary, because you are not changing anything. ichor status shows what Ichor decided at any point: what is in scope, what it questioned, and what was written anyway.
What actually moves the boundary.
Every prompt is read against the boundary currently held, and lands in one of four answers. Ambiguity always resolves to change nothing, because a boundary redrawn for no reason is worse than one held a turn too long.
| Your prompt | What happens |
|---|---|
| Names a path or a symbol outright | The boundary is redrawn around it. "fix the retry in lib/billing/invoice.ts" — what you pointed at wins over everything else in the sentence. |
| Names a directory outside the boundary | The boundary widens to include it, and keeps what it already had. A word that merely happens to match a folder name can never destroy the real task. |
| Ordinary words, already inside the boundary | Nothing changes. This is most prompts, and it is the correct answer for them. |
| A wall of common words, or a pasted stack trace | Nothing changes. A term matching a hundred and thirty-five files is telling you it is an English word, not where the work is. |
ichor start "…" names the task by hand. Detection still reports what it saw, but it will never redraw a boundary you chose yourself.
Every challenge cites something real.
When your agent is about to write something outside the job, it is stopped before the file exists, and told why.
⚠ ichor: this looks like scope expansion.
src/app/api/vendors/check-email/route.ts introduces a new POST endpoint at
/api/vendors/check-email that reaches Vendor, but the task's existing path
already reaches it:
POST /api/vendors → createVendor → Vendor.email (unique)
Why is a separate POST endpoint required?
If it genuinely is: ichor justify <file> "<reason>"
A path through your code, a table, a count of callers. Ichor never says "this looks unrelated" without showing what it looked at.
It is not a blocker. Your agent can answer with ichor justify and have the argument weighed, or simply explain and carry on — Ichor asks once per file. A file that was questioned and then written anyway is remembered as exactly that, and is never later quoted back as proof that something else belongs.
| Verdict | Meaning |
|---|---|
| EXPECTED | This is the job. Silent. |
| CONNECTED | Not the job, but genuinely joined to it — one call away, or working on the same table. Silent. |
| NOT_JUDGED | Not a file Ichor reads, so it has no opinion. Silent, and honest about why. |
| SUSPICIOUS | Outside the job, with evidence. You get asked. |
| HUMAN_REVIEW | Ichor cannot tell. It asks you rather than deciding. |
Running it, and asking it things.
Running it
| Command | What it does |
|---|---|
| ichor init | Install the hooks and tools in this repo, and write the database stack. |
| ichor up | Start HydraDB and MinIO locally. |
| ichor watch | Read the codebase and follow the conversation. The usual way to run it. |
| ichor start "…" | Name the task by hand. Detection still reports, but never redraws a boundary you chose. |
| ichor status | What is in scope, what was questioned, what was written anyway. |
| ichor stop | End the task and stop watching. |
| ichor key | Store your own OpenRouter key, or show whether one is set. Optional — see below. |
| ichor down | Stop the database, from any directory. --wipe also deletes the graph — that discards every project stored in it, not just this one, and Ichor says so before doing it. |
Asking it things
The same answers your agent gets from the MCP tools, in a shell. These work whether or not the workspace is trusted, which the MCP path does not.
| Command | What it answers |
|---|---|
| ichor find <words…> | Where does this live? A plain description instead of a grep pattern. |
| ichor impact <symbol> | What else is affected if this function or type changes. |
| ichor callers <symbol> | What calls this function, and from which endpoints. |
| ichor paths <model> | How the app reaches a table, and through what. --route narrows it. |
| ichor check <file> | Is this file part of the current task, and why. |
| ichor justify <file> "…" | Argue that a file outside the task is genuinely required, and have it weighed. |
Half of Ichor is not policing at all.
These exist because your agent is about to grep for something the graph already knows. In a real session it searched for five words, got 116 hits, and read six entire files to work out where the task lived. Ichor had the answer in 14ms.
| Tool | What it answers |
|---|---|
| ichor_find | Where does X live? Describe it in plain words — no need to guess a name. |
| ichor_impact | What breaks if I change this? Callers, endpoints, tables at stake. |
| ichor_callers | Who reaches this function, and from which endpoints. |
| ichor_paths | How the app reaches a table, and through what. |
| ichor_task_status | Is there an active task, and what is in it. |
| ichor_get_scope | The task neighbourhood, with distances. |
| ichor_check_change | Classify a file before writing it. |
| ichor_explain | Why this verdict, with the paths behind it. |
| ichor_request_scope_expansion | Argue for the boundary to grow. |
These search structure, not text: they rank the repository's own declarations and skip matches in comments and strings. They match names, not meaning — a description sharing no vocabulary with the code will not find it.
Ichor works fully without one.
Every boundary, every challenge, every piece of evidence and all nine tools your agent can call need no key and make no outbound request. Your code never leaves your machine.
A key adds exactly one thing. When your agent argues that an expansion is genuinely necessary, that argument can be weighed against the evidence the graph produced. Without a key, an argument Ichor cannot verify comes to you instead of being granted — which is a reasonable default, not a broken feature.
ichor key sk-or-… # get one at https://openrouter.ai/keys ichor key # is one set, and where did it come from? ichor key --remove # delete it
The key is checked with OpenRouter before it is stored, so a truncated paste is caught immediately rather than turning into a feature that quietly does nothing.
Cost is bounded by design. The Judge is never consulted on an ordinary edit — only when an argument has actually been made — and it is capped per task and per file, so a long session cannot run away with your credit.
None of these are required.
Ichor picks sensible values and ichor init writes the rest.
| Variable | Default | Purpose |
|---|---|---|
| ICHOR_OPENROUTER_KEY | — | Your OpenRouter key. OPENROUTER_API_KEY and OPENROUTER_KEY are also read. Beats the stored key. |
| ICHOR_JUDGE_MODEL | openai/gpt-5-mini | Which model weighs an argument. A cheap fallback is always tried second. |
| ICHOR_JUDGE_MAX_PER_TASK | bounded | How many times one task may consult the Judge. |
| ICHOR_JUDGE_MAX_PER_FILE | bounded | How many times one file may. |
| ICHOR_JUDGE_TIMEOUT_MS | 20000 | Give up on the model and fall back to the graph-only verdict. |
| ICHOR_HYDRA_URL | bolt://127.0.0.1:7687 | Where HydraDB is listening. |
| ICHOR_HYDRA_TOKEN | generated | Auth token for the local database. Written by ichor up. |
| ICHOR_HYDRA_NAMESPACE | default | Namespace inside HydraDB. |
| ICHOR_DEBUG | — | 1 mirrors the hook log to stderr while you work. |
When something looks wrong.
Ichor said nothing at all
It fails open on purpose: no task, no database, a parse error, a timeout — every one of those allows the edit. A tool that blocks work when it breaks gets uninstalled within the hour.
So it always writes down why. .ichor/hook.log in your repo has one line per decision, and ichor status shows the current state. A turn where the graph could not be reached says so to your agent as well, rather than leaving the session quietly unwatched.
"HydraDB is not answering on the Bolt port"
ichor up, and give Docker a moment. ichor status will tell you once it is reachable. After a container restart the first query can be slow enough to time out; the next one succeeds.
The agent still asks permission for Ichor's own tools
Claude Code ignores permissions.allow from a project's settings until the workspace is trusted, and untrusted is the default for a fresh clone. Open claude interactively in the repo once and accept the trust dialog. The hooks and every verdict work either way — and the shell commands in section 05 answer the same questions without any of this.
It did not notice when I switched tasks
Name the file or the folder you mean. An ordinary sentence of common words matches thousands of files on a large repo, so it cannot say where you have moved to. "fix the retry in backend/src/services/x/y.ts" is noticed; "fix the retry" is not. ichor start "…" always works.
Codex runs no hooks
Codex asks you to trust a hook file the first time it sees one, and codex exec cannot show that prompt — so in non-interactive mode it silently runs none. Use interactive codex and approve it once. That is Codex's behaviour rather than Ichor's, but it is worth knowing before concluding the integration is broken.
It questioned something it should not have
Measured on 30 real commits from a real product, that happens on about one edit in eight. Answer with ichor justify, or tell your agent why and carry on — Ichor asks once per file. If a boundary is plainly wrong, ichor start "…" names the task yourself and detection stops redrawing it.
What it cannot do.
A tool that claims to know where a task belongs should be precise about where it is blind.
- TypeScript and JavaScript only. Python is next. Nothing else is claimed.
- It understands one stack deeply. Next.js App Router routes and Prisma models are what it reads. Pages Router routes and other ORMs are not in the graph yet, so on those codebases Ichor sees the calls but not the endpoints or the tables — and ichor paths answers from the endpoints it does have.
- It still questions about one edit in eight that you were right to make. Measured, not estimated. Naming the file in your prompt is what makes the boundary precise: on the 1,362-file repository, 18 files of guesswork down to the 1 you pointed at.
- A shell write is challenged after it lands, not before. Ichor hooks the agent's edit tools, so a file written by cat >, a heredoc or your own editor has no pre-write hook. At the end of the command Ichor reads what changed on disk and asks then — before anything is built on top of it.
- Static analysis, and it says so. Dynamic dispatch and runtime-constructed calls cannot be seen, so results are a floor and never a ceiling. A call Ichor cannot resolve is counted and reported, never guessed — on a 1,362-file repo that was 7,178 call sites it declined to invent an edge for.
- Ichor can be wrong. The boundary is an expectation, not a fact. When it cannot verify a justification it asks you rather than deciding — and it never blocks a prompt.
Smaller ones, for completeness: a retrieval call gives up after 1.5 seconds and hands the question to your agent's own search; the graph is rebuilt between turns, so within a single turn Ichor can be a few edits behind; and two agent sessions in one checkout share a boundary, with the newer taking it over.