token-goat 2.9.1 → 2.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -717
- package/SECURITY.md +1 -1
- package/dist/{token-goat-chunk-LHMC5ENL.mjs → token-goat-chunk-CGWACYYZ.mjs} +1 -1
- package/dist/{token-goat-chunk-SXSWQMNP.mjs → token-goat-chunk-FXAPKVRG.mjs} +142 -69
- package/dist/{token-goat-chunk-V5T3YNYR.mjs → token-goat-chunk-JMUBXBG7.mjs} +5 -5
- package/dist/{token-goat-chunk-SQSB5P5P.mjs → token-goat-chunk-KKIB7O3Z.mjs} +172 -9
- package/dist/{token-goat-chunk-CD6U5OD6.mjs → token-goat-chunk-LEBYARO3.mjs} +3 -3
- package/dist/{token-goat-chunk-FDXINYK4.mjs → token-goat-chunk-LJ3CHCTT.mjs} +101 -18
- package/dist/{token-goat-chunk-4KD2N46D.mjs → token-goat-chunk-NY4HYRSK.mjs} +2 -2
- package/dist/{token-goat-chunk-RRNRPCLX.mjs → token-goat-chunk-Q4LOQY44.mjs} +60 -9
- package/dist/{token-goat-chunk-4ZDRTNGV.mjs → token-goat-chunk-UZ2NFOOZ.mjs} +165 -26
- package/dist/{token-goat-chunk-MR7MNSDR.mjs → token-goat-chunk-VCNW7BGU.mjs} +2 -2
- package/dist/{token-goat-chunk-AUM22UQ2.mjs → token-goat-chunk-ZVN5WHTO.mjs} +5 -5
- package/dist/token-goat-hook.mjs +5 -5
- package/dist/token-goat.core.mjs +5 -5
- package/docs/C4_RUNTIME_ARCHITECTURE.md +303 -0
- package/docs/cli.md +339 -0
- package/docs/install.md +331 -0
- package/docs/security.md +102 -0
- package/package.json +7 -2
package/docs/install.md
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Install guide"
|
|
3
|
+
description: "How to install, wire it into Claude Code, Codex, Gemini, Qwen, Kimi, opencode, OpenClaw, pi, Copilot, Grok, Cline or Cursor, and what lands on your machine."
|
|
4
|
+
image: /token-goat/assets/goat-social.png
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
[← Back to the token-goat README](../README.md)
|
|
8
|
+
|
|
9
|
+
# Install
|
|
10
|
+
|
|
11
|
+
> **Easiest install:** paste this repo's URL into your AI and ask it to install token-goat properly. It will run the commands, check codecs, and confirm everything is working.
|
|
12
|
+
|
|
13
|
+
**Requirements:** Node.js 22.16 or later (all platforms)
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
npm install -g token-goat
|
|
17
|
+
token-goat install
|
|
18
|
+
token-goat doctor # confirms hooks and sharp are working; look for "sharp: ok"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Three commands. Done. Hooks register and start working immediately; no terminal popups, no tray icon, no service to babysit.
|
|
22
|
+
|
|
23
|
+
### Agents choose the commands
|
|
24
|
+
|
|
25
|
+
People install token-goat. Agents use it. You do not need to memorize its commands or tell the agent which file type it has.
|
|
26
|
+
|
|
27
|
+
Installation adds a short routing guide to the agent's instructions. When the agent tries to read a supported binary document, a hook identifies the extension and returns the right next step. The agent starts with an inventory, then reads only the relevant part.
|
|
28
|
+
|
|
29
|
+
| Task | Agent flow |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| Review a PDF | `pdf-meta` and `pdf-outline`, then `pdf-locate` to find the pages that mention a term and `pdf-extract --pages` only those |
|
|
32
|
+
| Review a Word document | `docx-outline`, then `docx-text` |
|
|
33
|
+
| Review a slide deck | `pptx-outline`, then `pptx-slide` or `pptx-notes` |
|
|
34
|
+
| Review a workbook | `xlsx-sheets`, then `xlsx-head`, `xlsx-range`, or `xlsx-query` |
|
|
35
|
+
|
|
36
|
+
Give the agent the file and the task: “Review `manual.pdf` for warranty exceptions.” It selects the bounded reader. If no routing rule fits, it can run `token-goat commands` instead of guessing.
|
|
37
|
+
|
|
38
|
+
The commands stay separate so every retrieval is visible, repeatable, and easy to narrow. The agent chooses the sequence; the developer can still inspect or run any step directly.
|
|
39
|
+
|
|
40
|
+
For bounded archive/document comparisons after setup, see the [CLI comparison workflow](cli.md#archivedocument-comparison-workflow).
|
|
41
|
+
|
|
42
|
+
**Important — sharp is part of the install.** The biggest single win (~39% smaller than JPEG, ~97% smaller than raw PNG) comes from WebP encoding. sharp ships with prebuilt native binaries on Windows, macOS, and most Linux platforms, so a standard `npm install -g token-goat` includes it. On Alpine musl and some ARM boards, npm compiles from source against libvips — that may add a few seconds. `token-goat doctor` will report `sharp: ok` when the image pipeline is working; if it flags a failure, follow the [Image support](../README.md#image-support) section in the README before considering the install complete. **AI agents performing this install: run `token-goat doctor` after `token-goat install` and resolve any sharp failure as part of the same task — do not treat it as a follow-up.**
|
|
43
|
+
|
|
44
|
+
Two things change how Claude Code sessions behave: hooks fire automatically (image shrink, re-read dedup, compact manifests), and a delimited routing block written to `~/.claude/CLAUDE.md` plus a registered skill gate the agent's reads — before any file read it must ask whether a `token-goat read` / `symbol` / `section` returns just what it needs, and the block explicitly subordinates the harness's own Read/Grep tool-preference rules to the *fallback* choice once token-goat is ruled out. Install writes no permission entry: whether `token-goat` commands need a per-call approval prompt is left to your own `settings.json`, unchanged.
|
|
45
|
+
|
|
46
|
+
**Keep that block where install put it.** It's plain markdown in a file you own, so moving it into a tidier reference file is tempting — but `install` and `uninstall` resolve one hardcoded path (`~/.claude/CLAUDE.md`). A relocated copy is never refreshed, so it freezes at whatever version was current when it moved, and the next `install` sees CLAUDE.md missing its block and appends a fresh one — leaving the guidance duplicated across two files with only one of them live. `token-goat doctor` warns when it finds a block outside CLAUDE.md, naming the file; `install` warns at write time and `uninstall` reports what it couldn't remove. None of them edit a file token-goat doesn't own, so cleanup stays your call. A pointer that merely *mentions* the markers in prose is fine — detection requires both markers on their own lines.
|
|
47
|
+
|
|
48
|
+
The background indexer is not started by `install`. Run `token-goat worker start` on any platform to launch it as a detached process; `token-goat worker status` / `token-goat worker stop` manage it from there.
|
|
49
|
+
|
|
50
|
+
### Companion CLI tools (recommended — install these too)
|
|
51
|
+
|
|
52
|
+
token-goat covers the **narrow-read** half of cheap context: pulling one symbol, one section, one cached command output instead of a whole file. It does not cover the **deterministic-transform** half — searching wide, rewriting code structurally, converting data, running language tooling. Those belong in utilities, not in model output: an operation with a defined algorithm is reproducible, cheaper, and checkable against a spec rather than re-read for plausibility. Install these alongside token-goat so an agent has a real tool for each job instead of burning tokens simulating one.
|
|
53
|
+
|
|
54
|
+
**Priority tier**, the three that close actual gaps in a token-goat-only setup:
|
|
55
|
+
|
|
56
|
+
| Tool | Why it matters next to token-goat |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `ast-grep` | The symbol-aware **write** half. token-goat reads by symbol; ast-grep matches the AST and rewrites it (`--rewrite`, YAML rule files). Repo-wide renames, call-shape changes, and codemods become a reviewable diff instead of a model regenerating files. Unlike `rg`/`sd` it ignores comments and strings. |
|
|
59
|
+
| `uv` | One Rust binary replacing pip, pyenv, virtualenv, and pipx. Every Python env probe and validation cycle gets an order-of-magnitude faster, so verification stops being the slow step agents skip. |
|
|
60
|
+
| `ruff` | Python lint + format in one binary. Agent environment probes commonly emit `ruff check` as the Python verify command; without it installed that path silently degrades to no check at all. |
|
|
61
|
+
|
|
62
|
+
**Base stack**, what a read or a search falls back to once the gate has ruled token-goat out. The guidance token-goat writes names no binary on purpose, because an instruction-file loader will harvest backticked names into a tool allowlist and then warn that every one of them is unknown. So this list is a recommendation, not something the installed block depends on:
|
|
63
|
+
|
|
64
|
+
`rg` (search) · `fd` (file discovery) · `bat` (paged/piped reads) · `eza` (listings) · `delta` (diff rendering) · `jq` / `yq` (JSON / YAML) · `sd` (find-replace) · `mlr` (CSV/TSV/JSON records) · `sqlite3` (structured queries) · `gh` (PRs, issues, CI) · `hyperfine` (benchmarks) · `fzf`, `lazygit` (interactive)
|
|
65
|
+
|
|
66
|
+
Optional but useful: `difft` (difftastic — syntax-aware diff, so reformats and moved blocks stop generating review noise), `just` (task runner, keeps verify commands discoverable), `typos` (deterministic spellcheck).
|
|
67
|
+
|
|
68
|
+
For archive/document work specifically, token-goat's bounded SQLite, XLSX, and PDF readers are documented in the [CLI comparison workflow](cli.md#archivedocument-comparison-workflow); keep rendering and schema-specific lineage interpretation in dedicated document tooling.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# macOS / Linux (Homebrew)
|
|
72
|
+
brew install ast-grep uv ruff ripgrep fd bat eza git-delta jq yq sd miller sqlite gh hyperfine fzf lazygit
|
|
73
|
+
|
|
74
|
+
# Debian / Ubuntu — note the binary renames: rg=ripgrep, fd=fdfind, bat=batcat
|
|
75
|
+
sudo apt install -y ripgrep fd-find bat jq sqlite3 fzf pipx
|
|
76
|
+
pipx install uv && pipx ensurepath # pipx puts uv in ~/.local/bin
|
|
77
|
+
export PATH="$HOME/.local/bin:$PATH" # this shell; ensurepath covers later ones
|
|
78
|
+
uv tool install ruff
|
|
79
|
+
npm install -g @ast-grep/cli
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```powershell
|
|
83
|
+
# Windows (winget)
|
|
84
|
+
winget install BurntSushi.ripgrep.MSVC sharkdp.fd sharkdp.bat eza-community.eza `
|
|
85
|
+
dandavison.delta jqlang.jq MikeFarah.yq chmln.sd Miller.Miller `
|
|
86
|
+
SQLite.SQLite GitHub.cli sharkdp.hyperfine junegunn.fzf JesseDuffield.lazygit
|
|
87
|
+
winget install astral-sh.uv # then: uv tool install ruff
|
|
88
|
+
npm install -g @ast-grep/cli # provides `ast-grep` (the old `sg` alias is deprecated)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If `winget` is unavailable (common when a session runs under a service account rather than an interactive login), `uv` also installs via `python -m pip install uv`, and `ast-grep` only needs npm. Verify the whole set in one pass:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
for t in token-goat ast-grep uv ruff rg fd bat eza delta jq yq sd mlr sqlite3 gh hyperfine; do
|
|
95
|
+
command -v "$t" >/dev/null 2>&1 && echo "$t ok" || echo "$t MISSING"
|
|
96
|
+
done
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Codex CLI users
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
token-goat install --codex
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The `--codex` flag patches both Claude Code and Codex CLI in one pass.
|
|
106
|
+
|
|
107
|
+
### Gemini CLI users
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
token-goat install --gemini
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This writes hook entries into `~/.gemini/settings.json` using Gemini CLI's `BeforeTool` / `AfterTool` / `PreCompress` event names. Token-goat translates between Gemini's snake_case tool names (`run_shell_command`, `read_file`, `grep_search`, etc.) and its internal format automatically. Image shrinking, session hints, post-edit indexing, compact assist, and bash output compression all work. To remove: `token-goat uninstall --gemini`.
|
|
114
|
+
|
|
115
|
+
### Qwen Code users
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
token-goat install --qwen
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
This writes hook entries into `~/.qwen/settings.json`. Unlike Gemini CLI (its own ancestor, with a custom `BeforeTool`/`AfterTool`/`PreCompress` event/matcher scheme), Qwen Code's hooks system diverged and now mirrors Claude Code's own natively — `PreToolUse`/`PostToolUse`/`PreCompact`/`UserPromptSubmit`/`SubagentStop` event names and snake_case stdin JSON — so token-goat wires all five events with no event-shape translation. Tool names still need translating: Qwen Code's payloads carry its own runtime tool ids (`read_file`, `run_shell_command`, `grep_search`, ...), which token-goat maps to its internal tool vocabulary from Qwen Code's own tool-name source. token-goat uses a catch-all matcher per event rather than an incomplete per-tool list. Image shrinking, session hints, post-edit indexing, compact assist, and bash output compression all work. This bridge was built from QwenLM/qwen-code's published docs, not tested against a live Qwen Code install — if hooks aren't firing, `token-goat doctor` and the settings.json contents are the first things to check. To remove: `token-goat uninstall --qwen`.
|
|
122
|
+
|
|
123
|
+
### Kimi Code users
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
token-goat install --kimi
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
This writes `[[hooks]]` entries into `~/.kimi-code/config.toml` (or `$KIMI_CODE_HOME/config.toml`), covering Kimi Code's `PreToolUse`, `PostToolUse`, `PreCompact`, `UserPromptSubmit`, `SubagentStop`, and `SessionStart` events. Kimi Code sends a Claude-Code-shaped snake_case payload on stdin, but it reads a different response: only a top-level `message` and `hookSpecificOutput.permissionDecision` / `permissionDecisionReason`. So the install also writes a small shim at `~/.kimi-code/hooks/token-goat-shim.js` that translates token-goat's answer into that contract, turns a hint into `message`, and writes nothing at all for a no-op. Image shrinking, session hints, post-edit indexing, compact assist, and bash output compression all work. `Notification` and `Stop` are not wired, because token-goat has no handler for them. Input and output rewriting are not wired either: Kimi Code offers no channel to replace a tool's input or its result. This bridge was built from MoonshotAI/kimi-code's own source and docs, not tested against a live Kimi Code install, so if hooks are not firing, `token-goat doctor` and the `config.toml` contents are the first things to check. To remove: `token-goat uninstall --kimi`.
|
|
130
|
+
|
|
131
|
+
### opencode users
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
token-goat install --opencode
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The `--opencode` flag patches Claude Code and drops a TypeScript bridge plugin into opencode's plugins directory — one command, no separate base install. Image shrinking, post-edit indexing, compact assist, and rewritten tool results (prompt-injection fencing, secret redaction, and output compression replace the raw result, the same protection Claude Code sessions get) work. So do repeat-search denial for `websearch`, repeat-load denial for `skill`, and the subagent prompt briefing for `task` — all three tool ids and their argument keys were verified against opencode's own source at the installed release's tag. Session hints don't — opencode's plugin API has no way to inject context before a tool read.
|
|
138
|
+
|
|
139
|
+
### openclaw users
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
token-goat install --openclaw
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The `--openclaw` flag patches Claude Code and registers a TypeScript bridge plugin with OpenClaw's gateway: it drops `~/.openclaw/plugins/token-goat.ts` and adds it to `~/.openclaw/openclaw.json`'s `plugins.load.paths` / `plugins.entries` (existing config is merged, never overwritten). OpenClaw's plugin SDK does support `before_tool_call`/`after_tool_call` hooks with the block/rewrite shape token-goat needs; unlike the other bridges, no argument-key remapping is needed at all, since OpenClaw's tool-call params are already snake_case (`file_path`, `command`, etc.) — the same keys token-goat's own `tool_input` uses.
|
|
146
|
+
|
|
147
|
+
What works: **bash output compression**, **re-read denial** and **surgical-read redirects for oversized first reads**, **image shrinking** (`before_tool_call` returns rewritten `params` whose `path` points at a materialized shrunk copy, the same mechanism the pi bridge uses), and **post-edit indexing** (all via `before_tool_call`/`after_tool_call`; OpenClaw's read/edit/write tools send the file path under `path`, which the plugin now forwards to token-goat as `file_path` too — earlier versions of this bridge assumed the keys already matched, so these read/edit hooks silently never engaged). What doesn't: **session hints** — OpenClaw's tool-call hooks have no context-injection channel, only param rewriting — and the **compaction manifest** — OpenClaw's `before_compaction`/`after_compaction` are observation-only, with no return-value mechanism to inject a manifest into the next turn the way pi's compaction hooks do.
|
|
148
|
+
|
|
149
|
+
This bridge has not been validated against a live OpenClaw instance — it's built from OpenClaw's documented plugin SDK and hook event types, not tested against a real running gateway. If tool calls aren't being intercepted, the built-in tool name list in `openclaw.ts`'s `TOOL_TO_TG` map is the first thing to check. To remove: `token-goat uninstall --openclaw`.
|
|
150
|
+
|
|
151
|
+
### pi users
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
token-goat install --pi
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The `--pi` flag patches Claude Code and drops a TypeScript extension into pi's global extensions directory (`~/.pi/agent/extensions/token-goat.ts`). pi auto-discovers it on the next launch (approve the project-trust prompt the first time). The extension is a normal pi extension — a default-exported factory that subscribes to `session_start`, `tool_call`, `tool_result`, `session_before_compact`, and `session_compact` — and bridges those events into token-goat's `token-goat hook <event>` subprocess protocol.
|
|
158
|
+
|
|
159
|
+
What works: **bash output compression** (the bash command is rewritten in `tool_call`; pi's `powershell` tool, whose input schema is identical to its bash tool's, is bridged the same way), **re-read denial** and **surgical-read redirects for oversized first reads** (both return `{ block, reason }` from `tool_call` — a confirmed re-read, or a first read at/above the pressure-scaled `large_read_redirect_bytes` gate, pointing at `token-goat skeleton`/`section`/`symbol` instead), **image shrinking** (`tool_call` rewrites the read path in place to a materialized shrunk copy), **post-edit indexing**, **output caching** and **rewritten tool output** (all three from `tool_result`: a compressed or redacted result is returned to pi as replacement content, with any image blocks in the result left in place), and the **compaction manifest** (captured at `session_before_compact`, re-injected after `session_compact` since pi's compaction replaces rather than appends). Skill-overhead preservation does not apply — pi has no Skill tool; skills are template expansions. To remove: `token-goat uninstall --pi`.
|
|
160
|
+
|
|
161
|
+
**Project-local install (single project only).** pi also loads extensions from a project's `.pi/extensions/` directory (after the project is trusted). To install for one project without touching the global directory, drop the extension there:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npx token-goat install --pi --local
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
This writes `.pi/extensions/token-goat.ts` in the current project only. Remove it by deleting that file.
|
|
168
|
+
|
|
169
|
+
### Copilot CLI users
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
token-goat install --copilot
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The `--copilot` flag patches Claude Code and registers a Copilot CLI hook config: `~/.copilot/hooks/token-goat.json` (a `{ version, hooks }` file registering `sessionStart`, `preToolUse`, `postToolUse`, `preCompact`, `agentStop`, `subagentStop`, and `userPromptSubmitted`, per Copilot's own [hooks reference](https://docs.github.com/en/copilot/reference/hooks-reference)) plus the shim script it points at, `~/.copilot/hooks/token-goat-shim.js`. Unlike Codex, Copilot's event names and response schema (`permissionDecision`/`modifiedArgs` for `preToolUse`, `modifiedResult`/`additionalContext` for `postToolUse`, `decision`/`reason` for `agentStop`/`subagentStop`) genuinely differ from Claude Code's, so the shim translates rather than passes through.
|
|
176
|
+
|
|
177
|
+
What works: **the command-routing reminder** (`sessionStart` returns `additionalContext`, so Copilot is told token-goat exists before it picks its first read tool — this is the one channel that lands ahead of that decision), **bash output compression and re-read denial** (`preToolUse` returns `modifiedArgs` or `permissionDecision: "deny"`), **background-shell output compression** (`postToolUse` returns `modifiedResult`), **image shrinking** (`preToolUse` on a `view` call returns `modifiedArgs` carrying the full original arguments with `path` swapped to a materialized shrunk copy — Copilot replaces the tool call's arguments wholesale with `modifiedArgs`, so the rewrite must carry them all), **post-edit indexing** (a `postToolUse` side effect; it needs no response channel), and **stop-hallucination logging** (`agentStop`/`subagentStop` map a token-goat `deny` onto `decision: "block"`, everything else onto `decision: "allow"`). `preCompact` and `userPromptSubmitted` are notification-only on real Copilot CLI, per its docs: Copilot never reads a response body for either, so token-goat's compaction manifest and prompt-context hints have no surfacing channel there. The shim still calls through for both so token-goat's internal side effects keep running, but nothing gets injected back into the agent. Copilot's built-in tool names are remapped onto token-goat's internal names where a clear match exists (`view`→Read, `edit`→Edit, `create`→Write, `bash`/`powershell`→Bash, `read_bash`/`read_powershell`→BashOutput, `web_fetch`→WebFetch, `grep`→Grep, `glob`→Glob). MCP-server tool calls, which Copilot names `<server>-<tool>` rather than `mcp__<server>__<tool>`, are translated too, but only when the name matches Copilot's own cached tool list exactly — never guessed from the name's shape, because a server name can itself contain a hyphen and a wrong guess would make the read-only MCP dedup path deny an ordinary built-in call. With no cache to match against, nothing is translated. `memory`, `ask_user`, `write_bash`/`write_powershell` (which send keystrokes to a running shell, not commands), and `stop_bash`/`list_bash` pass through unmapped and simply no-op. `task`, Copilot's subagent tool, is not remapped either, but it is handled under its own name: a `task` spawn gets the same prompt briefing, duplicate-spawn advisory, and recall pointer on a long report that a Claude Code `Agent` spawn gets. The once-per-session unrestricted-spawn advisory is the one exception: it is suppressed under Copilot, because it rides the `postToolUse` `additionalContext` channel Copilot discards, and its `subagent_type` advice describes Claude Code's Task schema, which Copilot's `task` tool does not use.
|
|
178
|
+
|
|
179
|
+
**Why the background-shell compression matters most on Copilot.** Copilot runs shell commands in the background: a build or a test suite is started once, and the model then checks on it repeatedly while it runs. Each check hands back everything the command has printed since it started, from the first line. So the second check re-sends the whole first check, the third re-sends the first two, and a check ten minutes into a slow build re-sends the same output for the tenth time. The model has already read all of it and pays again for every word, every time. Token-goat sends the first check through untouched, then returns only the new part on each later check, with one line saying that is what it is; a check that found nothing new comes back as a single short line instead of the whole output again. Measured through the installed hook: a second check of 5,200 characters came back as about 1,250, and a third check that added nothing came back as 60 — roughly a quarter of the cost for the second look and about one percent for the third, improving the longer the command runs. Nothing is lost, because what is cut is what was already sent. It only shortens a check when the new output genuinely continues the last one seen; anything else passes straight through, so the worst case is a saving that does not happen rather than a wrong answer.
|
|
180
|
+
|
|
181
|
+
No ambient environment variable documents "this process is running under Copilot CLI" the way Codex/opencode set one, so the shim sets `TOKEN_GOAT_HARNESS_OVERRIDE=copilot_cli` itself before calling `token-goat hook` (same workaround `--pi` uses). Install also writes a token-goat routing block into `~/.copilot/copilot-instructions.md` (the same delimited-block gate written to `~/.claude/CLAUDE.md` and `~/.codex/AGENTS.md`), merged idempotently so any hand-written content outside the markers is preserved byte-for-byte. If you set `COPILOT_HOME`, install follows it — hooks go to `$COPILOT_HOME/hooks/` and the routing block to `$COPILOT_HOME/copilot-instructions.md`, matching where Copilot CLI actually reads them. To install for one project instead of user scope: `token-goat install --copilot --local` (writes `.github/hooks/token-goat.json` and `.github/copilot-instructions.md` in the current project). To remove: `token-goat uninstall --copilot`.
|
|
182
|
+
|
|
183
|
+
**If Copilot CLI starts denying every tool call with `Denied by preToolUse hook ... (hook errored)`:** this is Copilot's own fail-closed behavior for a `preToolUse` hook that crashes, exits non-zero, or returns unparseable output -- it isn't limited to token-goat's own tool calls, since a fail-closed `preToolUse` hook blocks the whole session. Copilot caches hook configs at session start, so **renaming or reinstalling the hook mid-session has no effect** -- the only recovery is: run `token-goat install --copilot` (or `token-goat doctor`, which now checks the installed hook end-to-end and calls out a stale node-binary path from an nvm/fnm/volta upgrade specifically), then **fully restart Copilot CLI**.
|
|
184
|
+
|
|
185
|
+
### Grok CLI (xAI Grok Build) users
|
|
186
|
+
|
|
187
|
+
Grok Build already reads Claude Code's `~/.claude/settings.json` as a "Harness Compatibility" source out of the box (confirmed against grok 0.2.93 and its own [hooks doc](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/10-hooks.md)), so `token-goat install` alone already gets most of the integration working — image shrinking, session hints, post-edit indexing, and bash output compression all fire. The one gap: Grok's own `PreToolUse` hook contract documents only `{"decision":"allow"}` / `{"decision":"deny","reason":"..."}`, never token-goat's harness-independent `{"decision":"block","reason":"..."}` shape (unlike Gemini CLI, whose docs explicitly confirm `"block"` as an accepted alias for `"deny"`), so re-read denial and oversized-first-read redirects don't reliably block on the Claude Code compat path alone.
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
token-goat install --grok
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The `--grok` flag patches Claude Code and additionally writes a standalone hook config at `~/.grok/hooks/token-goat.json` (global scope only — Grok's own project-scoped `<project>/.grok/hooks/*.json` requires a separate manual `/hooks-trust` grant this bridge can't perform for you) plus the shim it points at, `~/.grok/hooks/token-goat-shim.js`. The shim's only job is translating that one response shape: a token-goat `{"decision":"block",...}` deny becomes Grok's documented `{"decision":"deny",...}` (with exit code 2, matching Grok's own "explicit deny" convention), and every other event's response is forwarded through unmodified — Grok already sends the raw camelCase wire payload (`toolName`/`toolInput`/`sessionId`) token-goat's built-in `grok` harness detection (`GROK_SESSION_ID`, set on every hook subprocess Grok spawns) already normalizes correctly. That normalization maps every tool id registered in the grok 0.2.93 binary itself — both shell-tool spellings (`run_terminal_command` and `run_terminal_cmd`), `web_fetch`, `web_search`, `glob`, and the `hashline_*`/`*_concise` read/edit/grep variants — onto token-goat's internal tool names, so hooks fire regardless of which id a given Grok build sends.
|
|
194
|
+
|
|
195
|
+
To remove: `token-goat uninstall --grok`.
|
|
196
|
+
|
|
197
|
+
### Cline, Windsurf, Cursor, and other AI tool CLIs
|
|
198
|
+
|
|
199
|
+
No separate install step needed. Token-goat compresses the terminal output of these tools automatically as soon as they appear on your PATH. Run `token-goat doctor` to confirm they are detected — the "Third-party AI tools" section will show `detected — bash output compression active`.
|
|
200
|
+
|
|
201
|
+
Filters are built in for: **Cline** (`cline` / `claude-dev`), **Windsurf** (`windsurf`, including Cascade AI patterns), **Cursor** (`cursor`), **GitHub Copilot CLI** (`gh copilot explain/suggest` and the standalone `copilot` binary — this passive output filter is separate from the `--copilot` hook bridge above; it works with no install step and covers Copilot CLI's own terminal chrome, not the hook-driven read/index integrations), **Aider** (`aider`), **Continue** (`continue`), **OpenCode** (`opencode`). Each filter strips version banners, spinner/thinking lines, token-usage boilerplate, and tool-call progress noise while keeping the AI response body, error signals, and any user-approval prompts verbatim.
|
|
202
|
+
|
|
203
|
+
### Updating
|
|
204
|
+
|
|
205
|
+
There is no auto-update mechanism — token-goat never schedules or runs anything on its own. Updating is always a manual `npm install -g token-goat@latest`.
|
|
206
|
+
|
|
207
|
+
| When | Command |
|
|
208
|
+
|------|---------|
|
|
209
|
+
| Update now | `npm install -g token-goat@latest` |
|
|
210
|
+
| Reinstall from scratch (broken install, sharp failure) | `npm install -g token-goat@latest` |
|
|
211
|
+
|
|
212
|
+
### Upgrading from the Python version
|
|
213
|
+
|
|
214
|
+
The old Python package (`pip install token-goat`) wrote hook entries into `settings.json` with commands containing `token_goat` (underscore), invoking Python directly: something like `pythonw.exe -m token_goat.cli hook pre_tool_use`. The npm package invokes a generated shim instead (`"<node>" "~/.claude/hooks/token-goat-shim.js" pre_tool_use "<entry>"`).
|
|
215
|
+
|
|
216
|
+
Both `install` and `uninstall` recognize the older command spellings — `token_goat`, `tokenwise`, `tg-hook`, `token-goat-hook`, and the pre-shim `token-goat hook` — so you do not need to hand-edit `settings.json`. Installing replaces a stale entry in place rather than leaving a dead one beside the new one, and uninstalling removes it.
|
|
217
|
+
|
|
218
|
+
To upgrade cleanly:
|
|
219
|
+
|
|
220
|
+
1. Remove the Python package: `pip uninstall token-goat`
|
|
221
|
+
2. Install and register the npm package: `npm install -g token-goat && token-goat install`
|
|
222
|
+
3. Verify: re-run `token-goat install`, which should now report hooks already up to date, or use the `jq` check under [Verify](../README.md#verify).
|
|
223
|
+
|
|
224
|
+
**AI agents handling an upgrade:** complete all three steps in sequence. After step 3, confirm no `token_goat` entries remain in `settings.json` before reporting the upgrade done.
|
|
225
|
+
|
|
226
|
+
## What gets installed?
|
|
227
|
+
|
|
228
|
+
`token-goat install` writes the following on your machine — nothing else, anywhere. Every entry is reversed by `token-goat uninstall`. Integrations for other harnesses are additive on the way out as well as in, so a plain uninstall does not touch one you installed with `--codex`, `--copilot`, or a sibling flag: rather than undo something you did not ask about, it names each one still present and the flag that removes it. Run `token-goat doctor` at any time to see which of these are currently present.
|
|
229
|
+
|
|
230
|
+
**Claude Code integration** (`~/.claude/`)
|
|
231
|
+
|
|
232
|
+
| Path | What |
|
|
233
|
+
|------|------|
|
|
234
|
+
| `~/.claude/settings.json` | Hook entries for `SessionStart`, `PreToolUse` (Read/Grep/Bash, Drive/WebFetch), `PostToolUse` (Edit/Write/MultiEdit, Read/Grep/Glob, Bash, WebFetch, Skill), and `PreCompact`. Hook entries only: install writes nothing under `permissions`, so it never grants the agent unprompted execution of anything. Existing hooks are preserved; a timestamped `.bak` is written before any change.<br><br>The `PreToolUse` and `PostToolUse` matchers are narrowed to exactly the tools token-goat handles (plus `^mcp__`), generated from the live hook registry rather than a fixed list, so they can't fall out of date as handlers change. Claude Code starts a new process per matcher hit and most of that cost is process startup, so a catch-all matcher would make every unrelated tool call — `TodoWrite`, `TaskUpdate`, and friends — pay for a hook that has nothing to do. |
|
|
235
|
+
| `~/.claude/hooks/token-goat-shim.js` | The hook script those `settings.json` commands invoke (`"<node>" "<shim>" <event> "<entry>"`). It imports the hook library in-process instead of spawning a second process, and naming the node binary directly skips the npm bin wrapper — on Windows a `cmd.exe` layer every hook would otherwise pay for. Measured 480 ms → 324 ms per hook call. Regenerated on every `install` run. Always written here even for a `--project` install, since the command bakes in machine-specific absolute paths; a project-scope `settings.json` just points at this one. |
|
|
236
|
+
| `~/.claude/CLAUDE.md` | A delimited block (`<!-- token-goat-begin -->` … `<!-- token-goat-end -->`) telling the agent to prefer `token-goat read` / `symbol` / `section` over `Read` / `Grep`. Any existing content is preserved. |
|
|
237
|
+
| `~/.claude/skills/token-goat/SKILL.md` | The token-goat skill — the same routing guidance in skill form. |
|
|
238
|
+
|
|
239
|
+
**Background worker.** token-goat does not register any persistent OS-level autostart entry — no Windows registry `Run` key, no systemd user unit, no XDG `.desktop` entry, and no macOS launchd `.plist`. The worker that drains the reindex queue is started manually as a detached child process: `token-goat worker start` launches `node <npm-prefix>/lib/node_modules/token-goat/dist/token-goat.mjs --worker-daemon` and returns immediately, and the child keeps running independent of the parent shell. `token-goat worker status` reports whether it's running; `token-goat worker stop` kills it. If it crashes or is killed while the machine stays up, the next edit hook detects it's gone and respawns it automatically (checked on every edit, rate-limited to roughly once every 5 minutes). It does not survive a reboot or logout, though — re-run `token-goat worker start` after either.
|
|
240
|
+
|
|
241
|
+
There is no auto-update mechanism. Updating token-goat is always a manual `npm install -g token-goat@latest`.
|
|
242
|
+
|
|
243
|
+
**Data directory** (created on first run)
|
|
244
|
+
|
|
245
|
+
| Platform | Path |
|
|
246
|
+
|---------|------|
|
|
247
|
+
| Windows | `%LOCALAPPDATA%\dfk-helper\token-goat\` |
|
|
248
|
+
| Linux / WSL | `~/.local/share/token-goat/` |
|
|
249
|
+
| macOS | `~/Library/Application Support/dfk-helper/token-goat/` |
|
|
250
|
+
|
|
251
|
+
Contains the symbol index (`global.db`, per-project `.db` files), session cache, shrunken-image cache, cached skill bodies (5 MB cap, LRU-evicted), logs, locks, and the dirty-file queue. Nothing outside this directory and `~/.claude/` is written.
|
|
252
|
+
|
|
253
|
+
**What the index actually holds, in plain terms.** The point of a surgical read is returning a function body without the file around it, which means the database stores those bodies. `symbols.body` holds the source text of every indexed symbol, `symbols.docstring` its doc comment, `refs.context` the line around each reference, and `chunks.text` the passages that semantic search embeds. There is also a full-text index over the bodies and docstrings. So the database is not a list of names and line numbers: it is a substantial copy of your source, sitting in a plain unencrypted SQLite file outside the repository, at the path in the table above.
|
|
254
|
+
|
|
255
|
+
Three things follow, and they are worth knowing before you decide. It never leaves the machine: token-goat sends no telemetry of any kind, and the only outbound requests it makes at all are the ones listed in the security section, none of which carry index content. It is not protected by your repository's access controls any more, so anything on the machine that can read your home directory can read it, and on Linux and macOS that directory sits under a home that backup and sync tools routinely copy. And it outlives an uninstall unless you say otherwise: `token-goat uninstall --purge` deletes both roots and tells you how much it reclaimed.
|
|
256
|
+
|
|
257
|
+
**With `--codex`** (Codex CLI integration)
|
|
258
|
+
|
|
259
|
+
| Path | What |
|
|
260
|
+
|------|------|
|
|
261
|
+
| `~/.codex/config.toml` | Hooks block with Codex-specific matchers (`view_image|Bash`, `apply_patch`, `web_search`) plus `PreCompact`/`UserPromptSubmit`/`SubagentStop` global hooks. Existing hooks preserved. |
|
|
262
|
+
| `~/.codex/AGENTS.md` | A delimited block (`<!-- token-goat-codex-begin -->` … `<!-- token-goat-codex-end -->`) with the same routing guidance, adapted for Codex tool names. |
|
|
263
|
+
| `~/.codex/hooks/token-goat-shim.js` | The hook script `config.toml`'s hook commands invoke (`node "<path>" <event>`). Strips internal `_tg_*` keys and injects `hookSpecificOutput.hookEventName` to satisfy Codex's strict schemas. Regenerated on every `install --codex` run. |
|
|
264
|
+
|
|
265
|
+
**With `--gemini`** (Gemini CLI integration)
|
|
266
|
+
|
|
267
|
+
| Path | What |
|
|
268
|
+
|------|------|
|
|
269
|
+
| `~/.gemini/settings.json` | Hook entries under Gemini's `BeforeTool`, `AfterTool`, and `PreCompress` events, using Gemini's own snake_case tool-name matchers (`run_shell_command`, `read_file`, `grep_search`, etc.). Existing hooks preserved; a timestamped `.bak` is written before any change. |
|
|
270
|
+
|
|
271
|
+
**With `--qwen`** (Qwen Code integration)
|
|
272
|
+
|
|
273
|
+
| Path | What |
|
|
274
|
+
|------|------|
|
|
275
|
+
| `~/.qwen/settings.json` | Hook entries under Qwen Code's `PreToolUse`, `PostToolUse`, `PreCompact`, `UserPromptSubmit`, and `SubagentStop` events (Claude-Code-native names and payload shape, not Gemini's), using a catch-all matcher per event. Existing hooks preserved; a timestamped `.bak` is written before any change. |
|
|
276
|
+
|
|
277
|
+
**With `--kimi`** (Kimi Code integration)
|
|
278
|
+
|
|
279
|
+
| Path | What |
|
|
280
|
+
|------|------|
|
|
281
|
+
| `~/.kimi-code/config.toml` | `[[hooks]]` entries for Kimi Code's `PreToolUse`, `PostToolUse`, `PreCompact`, `UserPromptSubmit`, `SubagentStop`, and `SessionStart` events. Each entry carries only `event` and `command`, the keys Kimi Code's strict schema accepts. Existing hooks and other config keys preserved; a timestamped `.bak` is written before any change. |
|
|
282
|
+
| `~/.kimi-code/hooks/token-goat-shim.js` | The hook script those commands invoke. Rewrites a token-goat block into `hookSpecificOutput.permissionDecision` and a hint into a top-level `message`, and writes empty stdout for a no-op. Regenerated on every `install --kimi` run. |
|
|
283
|
+
| `~/.kimi-code/AGENTS.md` | A delimited block (`<!-- token-goat-kimi-begin -->` ... `<!-- token-goat-kimi-end -->`) with the routing guidance, adapted for Kimi Code tool names. |
|
|
284
|
+
| `~/.kimi-code/skills/token-goat/SKILL.md` | The same guidance as a Kimi Code skill. |
|
|
285
|
+
|
|
286
|
+
**With `--opencode`** (opencode plugin)
|
|
287
|
+
|
|
288
|
+
| Path | What |
|
|
289
|
+
|------|------|
|
|
290
|
+
| `~/.config/opencode/plugins/token-goat.ts` (Linux/macOS) or `%APPDATA%\opencode\plugins\token-goat.ts` (Windows) | TypeScript bridge plugin. Fires on `tool.execute.before`, `tool.execute.after`, and `experimental.session.compacting`. Covers image shrinking, post-edit indexing, and compact assist. |
|
|
291
|
+
|
|
292
|
+
**With `--pi`** (pi extension)
|
|
293
|
+
|
|
294
|
+
| Path | What |
|
|
295
|
+
|------|------|
|
|
296
|
+
| `~/.pi/agent/extensions/token-goat.ts` | TypeScript extension (default-exported `ExtensionAPI` factory). Subscribes to `session_start`, `tool_call`, `tool_result`, `session_before_compact`, and `session_compact`. Covers bash compression, re-read denial, pressure-scaled surgical-read redirects for oversized first reads, image shrinking, post-edit indexing, output caching, and the compaction manifest. A project-local install writes `<project>/.pi/extensions/token-goat.ts` instead. |
|
|
297
|
+
|
|
298
|
+
**With `--copilot`** (Copilot CLI hook bridge)
|
|
299
|
+
|
|
300
|
+
| Path | What |
|
|
301
|
+
|------|------|
|
|
302
|
+
| `~/.copilot/hooks/token-goat.json` | Hook config (`{ version, hooks }`) registering `preToolUse`, `postToolUse`, `preCompact`, `agentStop`, and `subagentStop`, each pointing at the shim script below. Existing files elsewhere in the hooks directory are untouched. |
|
|
303
|
+
| `~/.copilot/hooks/token-goat-shim.js` | The shim `token-goat.json`'s hook commands invoke (`node "<path>"`). Translates Copilot's event names and response schema (`permissionDecision`/`modifiedArgs`, `additionalContext`) to/from token-goat's internal hook protocol. Regenerated on every `install --copilot` run. A project-local install (`--copilot --local`) writes `<project>/.github/hooks/token-goat.json` and `<project>/.github/hooks/token-goat-shim.js` instead. |
|
|
304
|
+
| `~/.copilot/copilot-instructions.md` | A delimited block (`<!-- token-goat-begin -->` … `<!-- token-goat-end -->`) with the same routing gate written to `~/.claude/CLAUDE.md` and `~/.codex/AGENTS.md`, naming Copilot CLI's own `view`/`grep`/`glob` tools in the conflict-resolution clause. Merged idempotently — everything outside the markers is preserved byte-for-byte. A project-local install (`--copilot --local`) writes `<project>/.github/copilot-instructions.md` instead. |
|
|
305
|
+
|
|
306
|
+
**With `--grok`** (Grok CLI / xAI Grok Build hook bridge)
|
|
307
|
+
|
|
308
|
+
| Path | What |
|
|
309
|
+
|------|------|
|
|
310
|
+
| `~/.grok/hooks/token-goat.json` | Hook config (`{ hooks }`) registering `PreToolUse`, `PostToolUse`, `PreCompact`, `UserPromptSubmit`, and `SubagentStop` with an empty (match-everything) matcher, each pointing at the shim script below. Existing files elsewhere in the hooks directory are untouched; global scope only (Grok's project-scoped `.grok/hooks/` requires a separate manual `/hooks-trust` grant). |
|
|
311
|
+
| `~/.grok/hooks/token-goat-shim.js` | The shim `token-goat.json`'s hook commands invoke. Translates `PreToolUse`'s deny shape only (`{"decision":"block",...}` → Grok's documented `{"decision":"deny",...}`, plus exit code 2); every other event's response is forwarded unmodified. Regenerated on every `install --grok` run. |
|
|
312
|
+
|
|
313
|
+
**With `--vscode`** (VS Code MCP configuration; user scope by default, `-p`/`--project` for the workspace)
|
|
314
|
+
|
|
315
|
+
| Path | What |
|
|
316
|
+
|------|------|
|
|
317
|
+
| `%APPDATA%\Code\User\mcp.json` (Windows) / `~/Library/Application Support/Code/User/mcp.json` (macOS) / `~/.config/Code/User/mcp.json` (Linux) — or `<project>/.vscode/mcp.json` with `-p`/`--project` | Merges the `token-goat` stdio entry under VS Code's `servers` root key, preserving unrelated servers and settings. Refuses to write if the other scope already has a token-goat-managed entry, to avoid a duplicate registration. |
|
|
318
|
+
| `<project>/.github/copilot-instructions.md` | Adds a delimited VS Code routing block that documents supported MCP selection and explicitly says MCP does not intercept built-in file reads. |
|
|
319
|
+
|
|
320
|
+
**With `--hermes`** (Hermes Agent integration)
|
|
321
|
+
|
|
322
|
+
| Path | What |
|
|
323
|
+
|------|------|
|
|
324
|
+
| `~/.claude/settings.json` | No new entries beyond the base Claude Code install. Hermes delegates tasks to Claude Code via `claude -p '<task>'`, which loads hooks from this file normally. `token-goat install --hermes` verifies the hooks are present and reports the result. To remove the Hermes detection: `token-goat uninstall --hermes` (removes no files — Hermes shares the Claude Code hook entries). |
|
|
325
|
+
|
|
326
|
+
**With `--openclaw`** (OpenClaw plugin)
|
|
327
|
+
|
|
328
|
+
| Path | What |
|
|
329
|
+
|------|------|
|
|
330
|
+
| `~/.openclaw/plugins/token-goat.ts` | TypeScript bridge plugin (`definePluginEntry` registration). Subscribes to `session_start`, `session_end`, `before_tool_call`, `after_tool_call`, and `before_compaction`. Covers bash compression, re-read denial, pressure-scaled surgical-read redirects for oversized first reads, image shrinking, and post-edit indexing. Not validated against a live OpenClaw instance — see README's "openclaw users" section. |
|
|
331
|
+
| `~/.openclaw/openclaw.json` | Adds the plugin path to `plugins.load.paths` and an entry to `plugins.entries.token-goat`. Existing config preserved; a timestamped `.bak` is written before any change. |
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Security, privacy and uninstall"
|
|
3
|
+
description: "What goes over the network, what is stored locally, how untrusted pages are fenced against prompt injection, and how to uninstall and purge."
|
|
4
|
+
image: /token-goat/assets/goat-social.png
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
[← Back to the token-goat README](../README.md)
|
|
8
|
+
|
|
9
|
+
# Security, privacy, and uninstall
|
|
10
|
+
|
|
11
|
+
**No telemetry. No analytics. No background reporting or silent outbound connections.**
|
|
12
|
+
|
|
13
|
+
Outbound network is reserved to these explicit cases:
|
|
14
|
+
|
|
15
|
+
- Google Drive API calls, only if you already authorized Drive in Claude Code. Token-goat never prompts for its own auth.
|
|
16
|
+
- Image fetches from URLs: either explicit via `token-goat fetch-image <url>`, or when the AI agent issues a WebFetch call that returns image content — the hook intercepts and shrinks the image. The URL always originates from the agent's work, not from token-goat itself.
|
|
17
|
+
- `token-goat screenshot <url>` navigates a headless browser to the URL you give it, subject to the target restrictions described below.
|
|
18
|
+
- The first `token-goat semantic` run on a machine downloads the embedding model from `huggingface.co`, pinned to an immutable commit rather than a mutable branch and checked against a recorded SHA-256 and byte length before it is used, and only once `onnxruntime-node` has been installed (see below — it is not part of a default install). Subsequent runs use the local cache, re-verify it, and make no network call. Skip the download entirely by setting `indexing.embeddings_enabled = false` (it is on by default), in which case `semantic` falls back to full-text search.
|
|
19
|
+
- The first optical-character read of an image downloads the English language data (about 4 MB) from `cdn.jsdelivr.net`, at a fixed version path. Subsequent reads use the local cache. This happens for an explicit `token-goat image-text`, and also for the automatic text extraction the image-shrink hook performs when the agent reads a screenshot; turn the automatic one off with `image_shrink.ocr_enabled = false`.
|
|
20
|
+
|
|
21
|
+
**One switch for all of it.** Set `network.offline = true` (env `TOKEN_GOAT_OFFLINE`) and every one of the paths above refuses instead of connecting, saying so rather than failing quietly. Anything already cached keeps working: a machine that has the embedding model still runs `semantic`, and one that has the language data still reads text out of images. This is one of the settings a per-project config file may not touch, so cloning a repository cannot switch it back off.
|
|
22
|
+
|
|
23
|
+
**A repository cannot reconfigure the security controls.** A project-root `.token-goat.toml` layers on top of your global config, which is what it is for: hint thresholds, indexing settings, compression tuning. But that file arrives with the repository, so whoever wrote the repository wrote it. Seven whole sections are therefore off limits to it, plus one individual key, and come from your global config or the environment only: `injection` (prompt-injection fencing), `webfetch` (the fetch allow and deny lists), `gdrive` (the Google Drive integration), `mcp` (root confinement and the allowed-roots list), `network` (offline mode), `redaction` (the secret-redaction rules), `screenshot` (the headless browser), and the single key `indexing.cross_project_symbols`. A project file that sets one of them is ignored, and token-goat prints a line naming what it dropped. Everything else stays project-overridable.
|
|
24
|
+
|
|
25
|
+
**The lock covers the config file, not the environment.** These settings still read a `TOKEN_GOAT_*` environment variable, and a repository has ways to set one: a `.envrc` for direnv, a `terminal.integrated.env.*` block in a committed `.vscode/settings.json`, a `containerEnv` entry in a devcontainer. Refusing environment overrides would break the operator who exports a variable in their own shell, which is the legitimate case and the common one, so token-goat reports instead of refusing: `token-goat doctor` prints a `Security config overrides` line naming every locked security setting the environment is currently deciding, and the variable to unset. It covers all fifteen locked keys that read an environment variable, and it derives that set from the same two tables that define what a project config may not write, rather than from a list kept alongside them. Settings with a safe side (booleans) are reported when the environment holds them open; settings without one (lists, sizes) are reported whenever the environment supplies a value at all, since there is nothing to compare against. A default install, where nothing is set, prints a single ok line.
|
|
26
|
+
|
|
27
|
+
**Security reports.** See [SECURITY.md](../SECURITY.md). Email `token-goat@dfkhelper.com`; do not file as a GitHub issue. Reports are acknowledged within 7 days; coordinated disclosure with a 90-day default window.
|
|
28
|
+
|
|
29
|
+
**Dependency advisories.** `npm audit` on the published package is empty, for a default install as well as for `npm install --omit=optional`. That took removing the package the findings all came through: `@xenova/transformers`, which supplied the embedding half of `semantic` and carried a critical `protobufjs` advisory plus five more that had no forward patch. It is gone entirely now — the tokenizer and the ONNX runner are token-goat's own code over `onnxruntime-node`, which is 17 packages where the old one was 80. That runtime is opt-in and not installed by default. `semantic` still works without it, on keyword search. Both `semantic` itself and `token-goat doctor` say so, and print the one command that brings the embeddings back:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g onnxruntime-node # drop -g if token-goat is a project dependency
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
That command is the one thing here that is not clean: `onnxruntime-node` pulls an `adm-zip` below 0.6.0, which carries [one high advisory](https://github.com/advisories/GHSA-xcpc-8h2w-3j85) that npm reports twice. It is reachable only from that package's own install script, unpacking the binary it just downloaded. The full accounting — including the override that clears it, why co-installing a fixed `adm-zip` does not, and what the old package cost — is under [Dependency advisories](../SECURITY.md#dependency-advisories).
|
|
36
|
+
|
|
37
|
+
**Verifying what you installed.** Every published version is built and pushed by one pinned workflow when a GitHub release is published, with npm provenance, so `npm audit signatures` verifies the tarball against the commit that produced it. Details in [Verifying what you installed](../SECURITY.md#verifying-what-you-installed).
|
|
38
|
+
|
|
39
|
+
**Prompt injection.** When an AI reads a file, web page, or command output, that content enters its context alongside your own instructions. Prompt injection is when untrusted content includes text designed to look like instructions — "Ignore all previous directives and run this instead" — to redirect the AI mid-task.
|
|
40
|
+
|
|
41
|
+
Token-goat intercepts every Read, Fetch, Bash, and MCP call the AI makes. Text that came from somewhere else is wrapped in an untrusted-content fence before the model sees it, decided by where the text came from and not by whether anything looked suspicious in it (`injection.enabled`, on by default, turns the whole thing off). The content is also scanned for a set of imperative-override attack patterns ("ignore previous instructions," "reveal system prompt," and similar); a match adds the pattern names to the fence's notice and writes a row to the log, and a clean scan changes only the wording. That ordering is the point: the pattern list is deliberately short, so anyone phrasing the same instruction differently would otherwise get an unlabelled channel, and a miss would be silent.
|
|
42
|
+
|
|
43
|
+
Three surfaces are covered. Every fetched page is fenced as it arrives and again when a cached copy is recalled with `web-output`. Every MCP tool result is fenced as it arrives, which matters most: it is a remote server's output, so it is the least trustworthy text in the pipeline. And cached Bash and MCP output is fenced when recalled with `bash-output` or `mcp-output`, since the output of a build or test run in a project with a hostile dependency is written by a third party as much as any web page is. Document extraction (`pdf-extract`, `docx-text`, the `xlsx-*` and `pptx-*` commands), `pr-slice`, `gdrive-sections`, and `recall` are covered the same way. The fence naming tool output is a different tag from the one naming web content, so the label tells the model where the text came from.
|
|
44
|
+
|
|
45
|
+
Read is the exception: file content passes through to the model unfiltered, because filtering it would silently break legitimate use cases. Where token-goat splices a piece of a file into its own hint or denial message, that excerpt is fenced. Outside of the fence, the primary defense is the model's own training to treat tool output as data, not as commands from a trusted party.
|
|
46
|
+
|
|
47
|
+
One deliberate gap: `--json` output cannot carry a fence around the envelope, because a fence wrapped around JSON is no longer JSON and callers parse it. Those envelopes fence individual fields on a pattern match instead, since the fixed wrapper would otherwise cost more than a short field is worth. The printed (non-`--json`) form of the same command is always fenced.
|
|
48
|
+
|
|
49
|
+
Separately from that pass-through case: when a read hook *denies* a Read and substitutes its own message, any file bytes it embeds in that message (a markdown heading tree, a served compact or notebook sidecar, a re-read diff, a CSV header row, an HTML title) are wrapped in an `<untrusted-file-content>` fence first, so a hostile repo cannot get its own text presented to the model as token-goat speaking. That fencing is unconditional, not gated on the pattern scan — as all of it now is.
|
|
50
|
+
|
|
51
|
+
A third case needs no fence, because the danger is the line break rather than the wording. When token-goat prints its own summary of a file it prints one entry per line and takes the names and values straight out of that file: the column profile behind `csv-profile`, the key listing behind `json-outline` and `yaml-outline`, the entry listing behind `zip-list`, and any hook hint naming the file it is about. Every one of those values may legally contain a newline. A quoted CSV field spans lines by design, a JSON key is an arbitrary string, a zip entry name is whatever whoever built the archive wrote in the header, and a file name may contain a newline on Linux and macOS. So a single cell, key, entry or file name could end token-goat's line and start one of its own that reads exactly like another entry token-goat had written, with nothing but the line break to tell them apart. Control characters, Unicode line separators and format characters in those values are escaped into their visible form, so one entry stays one line and hostile content is shown rather than obeyed. The same rule covers a carriage return that would overwrite the line on screen, an ANSI escape that would recolour it, and a bidi override that would make the rest of it render backwards. Ordinary names and values pass through untouched. This matters most for an archive, since a `.whl`, `.vsix` or `.nupkg` comes from a package registry rather than from you.
|
|
52
|
+
|
|
53
|
+
The MCP tools (`symbol` when given a `file` filter, `read`, `section`, `skeleton`, `outline`, `refs`, `brief`, `grep`, `imports`, `exports`) are confined to the project root, resolving symlinks before the check. Set `mcp.confine_reads_to_project_root = false` (env `TOKEN_GOAT_MCP_CONFINE_READS`) in your global config if you genuinely need cross-root reads from an MCP client; a per-project file cannot set it. The CLI is deliberately unconfined and unchanged. This is defense in depth for one sink, not a sandbox: an agent that can call these tools can usually call its own read tool too.
|
|
54
|
+
|
|
55
|
+
Note what that flag does and does not cover. It stops a caller traversing *out of* the root it is given; it does not constrain *which* root the caller supplies. Every MCP tool takes an optional `projectRoot`, and it exists for a reason — the server's cwd is often not the workspace root for MCP clients — but tool arguments are model-generated, so that choice is untrusted input like any other. If your deployment treats MCP as the only path to the filesystem, set `mcp.allowed_roots` (env `TOKEN_GOAT_MCP_ALLOWED_ROOTS`, delimiter-separated like `PATH`) to the roots that may legitimately be named; a resolved root outside every entry is then refused. It is empty by default, which keeps the multi-root behavior above unchanged.
|
|
56
|
+
|
|
57
|
+
**Restricting what token-goat may fetch.** `webfetch.allow` and `webfetch.deny` (env `TOKEN_GOAT_WEBFETCH_ALLOW` / `TOKEN_GOAT_WEBFETCH_DENY`, comma-separated) are wildcard URL patterns that decide which addresses may be reached. Deny is checked first and wins; a non-empty allow list refuses anything it does not name. Patterns are matched against the address as it will actually be sent, not only as you typed it, so a trailing dot on the host, `..` path segments, a default port written out, and percent-encoded path characters cannot be used to step around a rule. Writing a default port in a pattern (`https://example.com:443/*`) and omitting it are equivalent. Both are empty by default, which permits everything, exactly as before. They apply to the WebFetch call your AI makes, to the fetches token-goat performs itself (`fetch-image`, `gdrive-sections`), and to the headless browser behind `screenshot` (whose page sub-resources are checked too), including every redirect hop, so an allowed site cannot redirect the request on to a denied one.
|
|
58
|
+
|
|
59
|
+
**Where cached content lives, and who can read it.** Cached command output, fetched pages, MCP results, session state and the source index all sit under one data directory (`~/.local/share/token-goat` on Linux, `~/Library/Application Support/token-goat` on macOS, `%LOCALAPPDATA%\dfk-helper\token-goat` on Windows). On POSIX that directory is created owner-only (mode 0700), and an existing one is tightened on the next run, so other local users on a shared build host cannot read it. Windows uses inherited ACLs instead. Individual JSON blobs are additionally written 0600.
|
|
60
|
+
|
|
61
|
+
**Confining `symbol` to one project.** `token-goat symbol` is the one read command that answers from the machine-wide index (`global.db`) rather than the current project, so by default `symbol <name>` and `symbol --grep .` return matching symbols, bodies included, from every project ever indexed on the host. That is deliberate and useful on a personal machine: it is how you find a helper you wrote in another repo. On a shared build host, or under an agent you have confined to one directory, it is a read channel that the directory sandbox does not close, because the answer comes out of the index instead of the filesystem. Set `indexing.cross_project_symbols = false` (env `TOKEN_GOAT_CROSS_PROJECT_SYMBOLS`) and `symbol` only answers from the project it is run in. `--project` and `--file` pointing outside that project are refused rather than honored, so the setting cannot be stepped around from inside the confined process. Every other read command (`read`, `refs`, `callers`, `types`, `dead`, `find`, `semantic`) is already project-scoped and is unaffected.
|
|
62
|
+
|
|
63
|
+
**Redacting credentials this build has never heard of.** Before anything is written to disk or handed back to the model, token-goat redacts the credential shapes it recognises — 19 patterns covering AWS, GitHub, Slack, Stripe, OpenAI, Anthropic, Google, npm and Azure keys, private key blocks, JWTs, bearer and basic auth headers, presigned URL signatures, and credentials embedded in a URL. Two settings cover what a fixed list cannot. `redaction.custom_patterns` (env `TOKEN_GOAT_REDACTION_CUSTOM_PATTERNS`, one pattern per line) is a list of your own regular expressions, redacted as `[REDACTED:custom]` — for an in-house token prefix, an employee number, an internal account id. A pattern that does not compile is skipped and named by `token-goat doctor` rather than failing silently, because a redaction rule you believe is running and which is not is worse than none at all. `redaction.strict` (env `TOKEN_GOAT_REDACTION_STRICT`, off by default) additionally redacts long high-entropy strings that match nothing known: the shape of a credential nobody wrote a rule for. It is a heuristic, and it says so — it needs three of the four character classes and genuine randomness, which spares git SHAs and hex digests, but it will sometimes redact a base64 blob that was not a secret. Both are settings a per-project config file may not touch, so a checked-in `.token-goat.toml` cannot weaken redaction; an environment variable still can, which is what the `Security config overrides` line above exists to surface.
|
|
64
|
+
|
|
65
|
+
**Cloud metadata endpoints are refused whatever your lists say.** A URL pointing at a cloud instance's metadata service — `169.254.169.254` and the whole `169.254.0.0/16` link-local range, `metadata.google.internal`, `metadata.goog`, the bare `metadata` short name, `instance-data` and `instance-data.ec2.internal`, `100.100.100.200`, `fd00:ec2::254` — is blocked before the fetch starts, and neither an allow list nor a deny list can permit it. The link-local range is also recognised when it is written as IPv6. This is not done by listing encodings: an earlier version listed three by name and there are at least three more, including one (6to4) that carries the address in a different position entirely. Instead token-goat parses the address and reads the positions an IPv4 address can sit in, so an IPv4-mapped address (`::ffff:169.254.169.254`, including its hexadecimal spelling), an IPv4-compatible address, either NAT64 prefix, the RFC 2765 translated form and a 6to4 address are all refused alike. The stated cost: an ordinary IPv6 address whose last four bytes happen to spell a link-local address is refused too. Nothing allocates that shape, and the alternative cost is an encoding nobody enumerated reaching the credential endpoint. The check reads the hostname you supply, so it is a check on the address the agent asked for: it does not re-run against a redirect target, and it governs the URL argument of token-goat's own fetch path rather than every socket the machine can open. These addresses answer only from inside a cloud instance, and what they answer with is that instance's own role credentials, which makes them the classic target for a redirected fetch. This deliberately is not a default entry in `webfetch.deny`: a default is something you can remove without noticing, and a config file that drifted or was copied from an older install would then quietly reopen it. `localhost` and private LAN addresses are deliberately not on this list — fetching your own development server is ordinary work, and blocking it would cost you something real to defend against nothing.
|
|
66
|
+
|
|
67
|
+
**Turning off the Google Drive integration.** `token-goat gdrive-sections` is the only feature that talks to Google. Set `gdrive.enabled = false` (env `TOKEN_GOAT_GDRIVE_ENABLED`) and the command refuses before it opens a connection, and the routing guidance token-goat writes into CLAUDE.md, AGENTS.md, `copilot-instructions.md` and the installed skill stops naming it, so an agent is never told the command exists. Nothing else in token-goat contacts Google Drive, and it holds no Drive credentials: `gdrive-sections` fetches the public export URL of a document id you pass it by hand.
|
|
68
|
+
|
|
69
|
+
**Secret redaction in cached content.** Token-goat caches command output, fetched pages, and MCP results so it can serve them back later instead of re-running the work. Anything it writes to those caches is passed through a redactor first, so a credential that appeared in output does not sit on disk in plain text and does not get replayed into a later session. This is unconditional — there is no flag to turn it on, and it applies to cached Bash and Task output (including the command string itself, which is where an inline `--token=...` would otherwise land), fetched web content, MCP tool results and their labels, `compress-text`/`handoff` payloads, and the raw JSON disk cache. Recognized shapes: Anthropic, OpenAI, AWS, GitHub, Slack, Stripe, npm, and Google keys; JWTs; `Authorization: Bearer`/`Basic` headers; PEM private-key blocks; presigned-url signatures (AWS `X-Amz-Signature`, Google Cloud Storage `X-Goog-Signature`, Azure SAS `sig`); and generic `password=`/`secret=`/`api_key=` assignments in `.env`, connection-string, and query-string shape. A match is replaced by a `[REDACTED:<kind>]` marker naming which pattern fired.
|
|
70
|
+
|
|
71
|
+
Session state gets the same treatment. The per-session file records which urls were fetched and which `curl -o` downloads landed where, and a url carries credentials as readily as output does. The fetched-url list is redacted, so the compaction manifest can still name what was fetched without naming the key; the download list is keyed by a digest of the url instead, because its only consumer is an exact-match check and a redaction there would make two urls differing only in their key look identical. The fetched-url entry also redacts the prompt that was sent with the page, and carries a digest of the pair so redaction cannot merge two entries that differ only inside the redacted span. An entry written by an older version is rewritten into this shape the first time the file is read, so upgrading clears the credentials an old file was holding rather than keeping them for the life of the session.
|
|
72
|
+
|
|
73
|
+
Two honest limits. It is a pattern matcher, not a classifier: a credential in a format it does not recognize — an internal token shape, a bare high-entropy string with no `key=` prefix — is cached as-is. And it protects what token-goat *stores*, not what your agent reads in real time; a secret printed to the terminal was already in the model's context before any caching happened. Treat it as damage control on the cache layer, not a reason to relax about printing secrets.
|
|
74
|
+
|
|
75
|
+
**`screenshot` target restriction.** `token-goat screenshot` and the MCP-adjacent screenshot path only navigate to `http:`/`https:` URLs; loopback, link-local, private, unspecified, and cloud-metadata addresses (including IPv4-mapped IPv6 and NAT64-encoded forms) are refused by default. Every redirect hop and sub-resource the page pulls in is re-validated against the same policy, and the hostname is resolved and the validated address pinned into the browser's own resolver, so DNS rebinding — a name that resolves differently between the check and the browser's own lookup — cannot slip a private address through. This is controlled by `screenshot.block_private_targets` (env `TOKEN_GOAT_SCREENSHOT_BLOCK_PRIVATE_TARGETS`), on by default. One limit remains: cross-host sub-resources (images, scripts, frames from a different host than the page itself) are resolved and checked but not pinned, so a record that changes between the check and the browser's own lookup could still be followed for those.
|
|
76
|
+
|
|
77
|
+
In practice: if you're reading files from untrusted sources or fetching unknown URLs during a session, pay attention to any actions the AI takes immediately after. Unusual follow-on behavior — opening files it wasn't asked about, writing to unexpected locations — is a sign that something in the read content may have tried to redirect it.
|
|
78
|
+
|
|
79
|
+
**Windows Defender (optional, Windows only).** Real-time scanning slows indexing. To exclude the data folder, open PowerShell as administrator:
|
|
80
|
+
|
|
81
|
+
```powershell
|
|
82
|
+
Add-MpPreference -ExclusionPath "$env:LOCALAPPDATA\dfk-helper\token-goat"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`0x800106ba` means the prompt is not elevated; reopen as administrator. On enterprise-managed Windows (domain-joined / Intune), Defender exclusions may be locked by Group Policy. The command will fail; that is expected and harmless.
|
|
86
|
+
|
|
87
|
+
**Uninstall.**
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
token-goat uninstall
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Reverses everything in [What gets installed?](install.md#what-gets-installed): the hook entries in `settings.json`, the `CLAUDE.md` block, the skill directory. Add `--codex`, `--gemini`, `--opencode`, `--pi`, `--hermes`, `--openclaw`, `--copilot`, `--grok`, or `--vscode` to also strip those integrations. It does not stop a running worker; use `token-goat worker stop` for that. Nothing else on the system depends on it.
|
|
94
|
+
|
|
95
|
+
By default the data directories stay: the index took real time to build and a reinstall wants it back. Add `--purge` to delete them as well, which is what offboarding a machine needs:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
token-goat worker stop
|
|
99
|
+
token-goat uninstall --purge
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
That removes both roots (the data directory holding the index, caches, models and logs, and the home directory holding session state and the OCR cache), naming each one and how much it reclaimed. It refuses while the worker is running, because the worker would rewrite files under a directory being deleted.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-goat",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.2",
|
|
4
4
|
"description": "Surgical token-reduction companion for Claude Code and other AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/token-goat.mjs",
|
|
@@ -55,6 +55,10 @@
|
|
|
55
55
|
"dist/",
|
|
56
56
|
"README.md",
|
|
57
57
|
"SECURITY.md",
|
|
58
|
+
"docs/cli.md",
|
|
59
|
+
"docs/install.md",
|
|
60
|
+
"docs/security.md",
|
|
61
|
+
"docs/C4_RUNTIME_ARCHITECTURE.md",
|
|
58
62
|
"LICENSE",
|
|
59
63
|
"scripts/install-git-hooks.mjs"
|
|
60
64
|
],
|
|
@@ -76,6 +80,7 @@
|
|
|
76
80
|
"csv-parse": "^7.0.2",
|
|
77
81
|
"esbuild": "^0.28.2",
|
|
78
82
|
"eslint": "^10.8.1",
|
|
83
|
+
"eslint-plugin-regexp": "^3.3.0",
|
|
79
84
|
"exceljs": "^4.4.0",
|
|
80
85
|
"fast-xml-parser": "^5.9.3",
|
|
81
86
|
"html-to-text": "^10.0.1",
|
|
@@ -112,7 +117,7 @@
|
|
|
112
117
|
"protobufjs": "^8.7.2",
|
|
113
118
|
"deepmerge-ts": "^8.0.1",
|
|
114
119
|
"uuid": "^14.0.1",
|
|
115
|
-
"sharp": "
|
|
120
|
+
"sharp": "$sharp",
|
|
116
121
|
"unzipper": "^0.12.5",
|
|
117
122
|
"adm-zip": "^0.6.0"
|
|
118
123
|
}
|