How-To: Learning-Driven Command Rewriting
This guide shows how to use terraphim-agent to rewrite shell commands before
execution -- for example npm install -> bun add or pip install -> uv add -- by plugging a knowledge-graph-backed thesaurus into your AI coding
agent's tool-execution hook.
The mechanism composes three pieces that already exist in terraphim-agent:
- A Logseq-style knowledge graph of command synonyms under
~/.config/terraphim/docs/src/kg/(or any role-configured path). terraphim-agent replace-- Aho-Corasick replacement that rewrites text using a role's compiled thesaurus.- A plugin hook in your AI agent (OpenCode, Claude Code, etc.) that
intercepts every Bash tool call, pipes the command through
replace, and writes the result back into the tool's args.
Prerequisites
terraphim-agentonPATH(any recent release; 1.16.33 or later).- A role whose KG directory you control. The default ships with a
Terraphim Engineerrole pointing at~/.config/terraphim/docs/src/kg/. - An AI agent that exposes a
tool.execute.beforestyle plugin API (OpenCode has one; Claude Code exposes equivalent hooks via shell scripts).
1. Curate the knowledge graph
Each concept is one markdown file. The filename stem becomes the concept
key; the H1 heading provides the display name used as the replacement; the
synonyms:: line lists terms that should be rewritten to it.
Example ~/.config/terraphim/docs/src/kg/bun install.md:
Conventions that matter in practice:
- Filename uses spaces, not underscores if the concept has multiple
words. The matcher compares against the filename stem (
bun install). - Multi-word synonyms are supported.
python -m pip installis a valid synonym and is matched as a whole phrase; the Aho-Corasick automaton uses LeftmostLongest, so the longer phrase wins when a shorter one would also match. - Do not overlap synonyms across files. If both
uv.mdanduv add.mdclaimpip install, the behaviour becomes non-deterministic at rebuild time. Keep single-token synonyms in the short file (pip->uv) and multi-token phrases in the specific file (pip install->uv add). - Keep domain vocabulary separate from command vocabulary if you need both. Create a dedicated role with its own KG path rather than bleeding domain terms into shell commands.
Seed set shipped in this repo
The Terraphim Engineer role's KG now ships with these command files:
| File | Maps to | Covers |
| ------------------------------------ | -------- | ---------------------------------------- |
| bun.md | bun | npm, yarn, pnpm |
| bun install.md | bun add | npm install, yarn install, pnpm install, npm i, yarn add, pnpm add |
| bun run.md | bun run | npm run, yarn run, pnpm run |
| bunx.md | bunx | npx, pnpx, yarn dlx |
| uv.md | uv | pip, pip3, pipx |
| uv add.md | uv add | pip install, pip3 install, pip add, pipx install, python -m pip install |
| uv sync.md | uv sync | pip install -r requirements.txt |
2. Verify with the CLI
| Expected output:
Flags worth knowing:
--fail-open-- on any error, emits the input unchanged. Mandatory in hooks so a misconfigured terraphim-agent never wedges the agent.--json-- structured output withresult,changed,replacements. Use this if the hook needs to branch on whether anything changed.--format plain|markdown|wiki|html-- how the replacement is wrapped. Hooks wantplain.
3. Flush the cache after KG edits
Terraphim caches compiled thesauri in a SQLite database at
/tmp/terraphim_sqlite/terraphim.db (path configured by
crates/terraphim_settings/default/settings.toml). Editing a KG markdown
file does not invalidate this cache; replace keeps returning the old
mapping until you flush it.
Because /tmp/ is wiped on reboot, a fresh boot always gives the
up-to-date thesaurus.
4. Wire up the hook (OpenCode example)
OpenCode plugins expose tool.execute.before(input, output) where
output.args.command is the mutable shell command about to run. The same
pattern works in Claude Code via the PreToolUse hook script, just with
shell-stdin instead of a JS closure.
// ~/.config/opencode/plugin/terraphim-hooks.js
const REWRITE_MODE = process.env. || "suggest"
const REWRITE_ROLE = process.env. || "Terraphim Engineer"
const AUDIT_LOG = `/Library/Application Support/terraphim/rewrites.log`
// Narrow whitelist of commands whose argument grammar survives a synonym swap.
const REWRITEABLE_HEADS =
/^\s*\b/i
Design notes:
- Whitelist, not blacklist. Arbitrary shell is never rewritten. Only
commands whose head matches
REWRITEABLE_HEADSare candidates. - Suggest mode by default. Set
TERRAPHIM_REWRITE_MODE=applyonce you trust the diffs. Git commit rewriting always applies because commit messages are prose, not syntax. - Audit log. Every rewrite is logged tab-separated to
~/Library/Application Support/terraphim/rewrites.logso you can diff before flipping modes. - Fail-open. Each external call is wrapped in try/catch with
||fallbacks. If terraphim-agent is missing, commands pass through unchanged.
5. Confirm end-to-end
With the hook installed and the cache flushed, open your agent, ask it to
run npm install express, and inspect the audit log:
You should see a line like:
2026-04-15T11:32:51.129Z suggest pkg-mgr npm install express bun add expressIn suggest mode the command still executes as npm install express; in
apply mode the agent actually runs bun add express.
6. Capturing user corrections (preview)
terraphim-agent learn hook --format <claude|codex|opencode> has three
modes driven by --learn-hook-type:
post-tool-use-- the default, captures failed Bash commands as learnings. This is already wired into the OpenCode plugin'stool.execute.aftercallback.pre-tool-use-- warns if the command matches a past failure pattern. Does not block.user-prompt-submit-- scans the user's prompt for patterns like "use X instead of Y" or "prefer X over Y" and records aToolPreferencecorrection under~/Library/Application Support/terraphim/learnings/correction-*.md.
At present these corrections are stored but not yet fed back into the replacement thesaurus. Closing that loop is tracked as future work -- see the accompanying GitHub issue "Learning-driven command correction: Phase 2 & 3".
Troubleshooting
replace returns the original unchanged.
Run terraphim-agent search "<synonym>" --role "<role>" -- if the concept
appears, the KG is loaded but the synonym is not. Confirm the synonym is on
the synonyms:: line (case-insensitive; commas separate entries). Flush
the cache (section 3) and retry.
Failed to load thesaurus: NotFound("thesaurus_...") in stderr.
Cosmetic. The agent looked for a pre-compiled JSON thesaurus first, didn't
find one, and fell back to building from markdown. Expected on first run.
Hook does nothing in OpenCode.
Check the plugin loaded: grep terraphim-hooks ~/.local/share/opencode/log/$(ls -t ~/.local/share/opencode/log/ | head -1).
You should see a line like service=plugin path=...terraphim-hooks.js loading plugin. If absent, the plugin file is in the wrong directory --
OpenCode autoloads from ~/.config/opencode/plugin/ and
~/.config/opencode/plugins/.
Commands get double-rewritten on retry.
The hook only touches tool.execute.before; the agent does not loop back
through the hook on its own retries. If you see double rewrites, check
whether input.tool === "Bash" is spelt exactly -- OpenCode passes
"Bash", not "bash".