Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ needed on a Pro/Max subscription** ([details](docs/how-to/install-plugin.md)):
/context-guru:install
```

**If it ever breaks and Claude cannot fix it:** `~/.local/state/context-guru/context-guru-reset`
undoes the routing from a plain terminal — no working Claude session, no proxy, no network. Routing
every request through a local proxy means a failure there fails every request, including the ones the
uninstall skill would need, so the way out cannot itself be a skill.

`/reload-plugins` is what makes the `/context-guru:*` skills exist in this session; without it the
last line answers `Unknown command`. A new session does the same thing.

Expand Down
1,419 changes: 1,393 additions & 26 deletions context-guru-plugin/plugin_test.go

Large diffs are not rendered by default.

22 changes: 20 additions & 2 deletions context-guru-plugin/scripts/check-proxy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,32 @@ the proxy's flags, and a stale copy of that list is what made this note wrong be
Log from the last attempt: ${LOG}"
fi

# The escape hatch, named here because THIS is the moment it is for: the proxy is down, the next
# request hangs, and the advice this note used to end on was "edit the JSON by hand, or run a skill
# from a session that still works" — the second of which is unavailable to a user whose sessions are
# all routed through the same dead port. Only named when it is actually on disk; pointing at a path
# that is not there would be worse than saying nothing.
HATCH="${CONTEXT_GURU_STATE:-${XDG_STATE_HOME:-$HOME/.local/state}/context-guru}/context-guru-reset"
if [ -x "$HATCH" ]; then
ESCAPE="To stop routing entirely and get working immediately, run this in a terminal — it needs no
working Claude session, restores every settings file this plugin edited from a copy taken before the
first edit, and prints what it changed:

${HATCH}

Add --dry-run first if you would rather see the plan than take it."
else
ESCAPE="To stop routing entirely and get working immediately, remove env.ANTHROPIC_BASE_URL from
.claude/settings.local.json (or run /context-guru:uninstall from a session that still works)."
fi

cat <<EOF
context-guru: this project is routed through http://127.0.0.1:${PORT}/anthropic, and nothing is
answering there. **Your request will hang with no error message** — that is what a dead proxy looks
like from inside Claude Code, and it is why this note exists rather than a skill.

${HOW}

To stop routing entirely and get working immediately, remove env.ANTHROPIC_BASE_URL from
.claude/settings.local.json (or run /context-guru:uninstall from a session that still works).
${ESCAPE}
EOF
exit 0
735 changes: 735 additions & 0 deletions context-guru-plugin/scripts/reset.sh

Large diffs are not rendered by default.

579 changes: 575 additions & 4 deletions context-guru-plugin/scripts/settings.py

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion context-guru-plugin/scripts/start-proxy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,13 @@ command -v setsid >/dev/null 2>&1 || STARTER=(nohup) # macOS has no setsid
# was a 404. Its default DB path is `./context-guru-dashboard.db` — the current directory, i.e.
# the user's repository — so the path must be set explicitly or the plugin litters the project it
# was invited into.
STATE="${XDG_STATE_HOME:-$HOME/.local/state}/context-guru"
# CONTEXT_GURU_STATE first, like settings.py, reset.sh and check-proxy.sh. S10 in review: this was the
# ONE script that ignored it, which had two consequences. A user who relocates their state directory
# got the pidfile written somewhere else, and uninstall looks for it here — one way it loses track of a
# running proxy. And it defeated the test isolation this PR added: pinning CONTEXT_GURU_STATE does not
# isolate the hook tests if the hook they spawn does not read it, which is how a `go test` run wrote
# four pidfiles into a reviewer's real ~/.local/state/context-guru.
STATE="${CONTEXT_GURU_STATE:-${XDG_STATE_HOME:-$HOME/.local/state}/context-guru}"
mkdir -p "$STATE" 2>/dev/null || STATE="${TMPDIR:-/tmp}"
PIDFILE="${STATE}/proxy-${PORT}.pid"

Expand Down
46 changes: 42 additions & 4 deletions context-guru-plugin/skills/install/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ Three lines, not an essay. The user asked for an install; deliver one, and let t
- **The real risk: if the proxy is down, requests HANG** rather than failing — no output, no error.
A `UserPromptSubmit` hook detects that and restarts it. This is why the default scope is one
project, not the machine.
- `/context-guru:uninstall` reverses it, restoring any base URL it replaced.
- `/context-guru:uninstall` reverses it, restoring any base URL it replaced. If the routing
itself is what breaks, a skill cannot run — so the install also drops a plain-sh escape hatch
outside the plugin, and step 6 tells them where it is.

Fuller detail — preset behaviour, subscription vs metered billing, scope trade-offs — is in
`docs/how-to/install-plugin.md`. Point at it; do not recite it.
Expand Down Expand Up @@ -128,9 +130,22 @@ trade-off in one line each:
| This project, whole team | `.claude/settings.json` | breaks for everyone who clones the repo |
| Every project (`--global`) | `~/.claude/settings.json` | **every Claude Code session on the machine breaks** |

If the user passed `--global`, use the third and confirm once that they mean it, naming the
blast radius. `env` blocks merge per key across scopes, so a user-scope install is not clobbered
by a project that ships its own `env` block.
Project scope is the default because of that third row, and the asymmetry is worth being concrete
about: a project-scope mistake costs the user one project, while the same mistake machine-wide takes
out every session they could use to fix it — including every project that has nothing to do with
context-guru. Writing the project's own `.claude/settings.local.json` is also why overriding a
machine-wide base URL is safe here: `env` blocks merge per key, most specific first, so the project
file wins for this project and changes nothing anywhere else.

**The script enforces this; it is not left to you.** `settings.py add` refuses the machine-wide file
outright and exits 2 with `reason=user_scope_needs_flag` unless it is passed `--user-scope`. That
refusal used to live only in this paragraph, and a default that exists only in a prompt is not a
guardrail — it can be skipped or read differently, and what it guards against is a machine-wide
lockout. So if the user passed `--global`: confirm once, naming the blast radius, and only then add
`--user-scope` to the step 6 command. Never add it to satisfy an error you did not expect — an
unexpected `user_scope_needs_flag` means you are about to write the wrong file.

Removal is not gated, on purpose: `/context-guru:uninstall` has to be able to clean every scope.

### 4. Look before you write

Expand Down Expand Up @@ -305,6 +320,27 @@ which fixes it without touching the user's shell at all. Still mention the `PATH
want `context-guru-proxy` on the command line too.

- `result=added` — report the `backup=` path to the user. That is their undo.
- `reset_hatch=<path>` — **print this path verbatim, on its own line, in your summary.** It is the
escape hatch, and this is the only moment the user is certain to be able to read it: the failure
it exists for is "every request through the proxy fails", and in that state no skill can run,
including `/context-guru:uninstall`. It happened — a colleague's install left him at 401 on every
call with the documented undo path unavailable for exactly the reason he needed it. Tell him the
command, not the concept:

```
If Claude ever stops being able to talk after this, run: <the reset_hatch path>
```

It restores every settings file this plugin edited, from a copy taken before the first edit, and
needs no Claude, no network, no proxy and no plugin. `reset_hatch=unavailable` means the state
directory could not be written — say so plainly, because then their only undo is the `backup=`
path above.
- `reset_original=unavailable` — the routing is recorded but the hatch holds no copy of the file's
original CONTENT, so it can name the file and point at the timestamped backups but cannot restore
it. **Read `reset_original_reason=` and pass it on rather than guessing** — the usual cause is not
a fault: the project was already routed when the record was first created (a pre-hatch install,
or a state directory that was cleaned), and no copy of an unrouted version was ever takeable.
Say that plainly; it is not a reason to stop, and it does not appear on a normal first install.
- `result=conflict` — you skipped step 4, or the file changed. Go back and ask; only pass
`--force` once the user has said to replace that specific value. When they do, the replaced
value is recorded and `/context-guru:uninstall` puts it back — say so, because "we will take
Expand Down Expand Up @@ -373,6 +409,8 @@ purpose — exiting clears in-memory cache state. If they want a shorter one, th
- Dashboard: `http://127.0.0.1:<port>/dashboard/` — the four billed token tiers are where the
cache effect is visible.
- `/context-guru:status` for the numbers, `/context-guru:uninstall` to undo.
- The escape hatch from step 6, once more, as the last line of your summary. A user who has to
find it will be looking at this transcript with a session that cannot answer questions.

## Do not

Expand Down
16 changes: 15 additions & 1 deletion context-guru-plugin/skills/uninstall/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ fails.

If the user is here because something is broken, do step 1 first and explain afterwards.

**If they are here because Claude could not talk at all, they got here by luck** — the routing this
skill removes is what breaks the session that would run it. There is a plain-sh escape hatch for
that, installed outside the plugin at
`${XDG_STATE_HOME:-$HOME/.local/state}/context-guru/context-guru-reset` (also on `PATH` as
`context-guru-reset` when `~/.local/bin` is there). It restores every settings file the install
edited from a copy taken before the first edit, and needs no Claude, no network and no proxy. Point
at it when a user reports being stuck, and prefer it to this skill for anyone who is currently
locked out — it is `cp`, where this skill is a conversation.

## 1. Remove the routing key

Check all three scopes: the install may have written any of them, and a `--global` install
Expand Down Expand Up @@ -132,7 +141,12 @@ Ask before either of these; neither is implied by "stop routing my sessions":
proxy.
- **Delete the state directory** — `~/.local/state/context-guru` holds the pidfile and the
dashboard database (session metadata and token counts, no prompt content unless they enabled
content capture). Nothing reads it once the proxy is gone.
content capture). **Say what else is in there before they agree:** `originals/` holds the copy of
each settings file taken before this plugin first edited it, `prereset/` holds copies taken by the
escape hatch, and `context-guru-reset` is the hatch itself. Deleting the directory is what makes
the routing removal irreversible and leaves them with no hatch — fine once they are working again
and have confirmed it, but it is not the tidy-up-the-leftovers item it looks like. The settings
copies are also the reason it is `0700`: they can contain a credential.

## 4. Confirm the end state

Expand Down
104 changes: 104 additions & 0 deletions docs/how-to/install-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ session works too; reloading is just quicker.

### Step 2 asks you to pick a scope — this is what it means

**Project scope is the default, and the script now enforces it.** `settings.py add` refuses to write
the machine-wide `~/.claude/settings.json` unless it is explicitly passed `--user-scope`, which the
install skill adds only after you have asked for `--global` and confirmed it. The reason is the
blast radius, not tidiness: a project-scope install that goes wrong costs you that project, while
the same mistake machine-wide takes out every Claude Code session you have — including the ones you
would use to fix it, and every project unrelated to context-guru. Routing per repo also means a
machine-wide base URL of your own keeps working everywhere else, because `env` blocks merge per key
with the most specific file winning.


`/plugin install` offers three, and they decide who gets **the plugin**:

| Option | Written to | Who gets it |
Expand Down Expand Up @@ -363,8 +373,102 @@ cold. The countdown is Claude Code's own view of *its own* last request; it has
ping the proxy sent while you were not typing. Treat `ka Np` and the next turn's latency as the
ground truth once keep-alive is running, not the countdown by itself.

## The escape hatch: when Claude Code cannot fix it for you

`/context-guru:uninstall` is a skill, so it needs a working session. The failure this plugin can
cause takes that away: every request goes through a local proxy, so a dead port, a stale routing
key or a credential the upstream rejects makes *every* API call fail — and the agent that would
undo the routing cannot reach a model to be asked. That is not hypothetical; it is the incident
this section exists because of.

So the install writes a hatch you can run yourself, and it lives **outside the plugin** — a
recovery tool inside the thing that broke is gone the moment you `/plugin uninstall`, refresh the
marketplace, or the plugin cache is wiped:

```bash
~/.local/state/context-guru/context-guru-reset # always here
context-guru-reset # on PATH too, when ~/.local/bin exists
```

It is POSIX `sh`. No Claude, no network, no proxy, no Python, no Go binary, no plugin code.

```bash
context-guru-reset --dry-run # show what it would change, write nothing
context-guru-reset # show the plan, ask, then restore
context-guru-reset --yes # no prompt, for a script
```

What it does:

1. reads its record of every settings file the install edited — kept in
`~/.local/state/context-guru/reset-manifest.tsv`;
2. copies each of those into `~/.local/state/context-guru/prereset/`, so running the hatch is itself
reversible. Deliberately **not** beside the settings file: these are complete copies of a file that
can hold a credential, and the old location dropped a new family of them inside your project's git
working tree, where `git status` noticing them was the only thing between that and a committed
secret;
3. restores each one from a copy taken **before the first edit**, held in
`~/.local/state/context-guru/originals/` — or deletes the file, when the install is the reason
it exists;
4. verifies no routing key is left, and reports anything it could not fix.

Why a separate copy when every edit already writes a `*.context-guru-backup-*` beside the file:
those are capped at ten and every add *and* remove writes one, so on a machine that has installed
and uninstalled a few times the backup holding your pre-context-guru state is the first to be
deleted. The copy under `originals/` is written once, with `O_EXCL`, and never pruned.

It never signals a process, never touches the network, and never edits a file it has no record of
editing. Any proxy still running exits on its own idle timeout.

### What it prints, and what it will not print

The plan shows a diff before it asks, because a restore reverts the whole file — including permission
grants Claude Code appended as you approved tools. That output is filtered, and the filter is an
**allowlist** for `"key": value` lines: values are printed only for keys known to be safe (the
routing keys, `model`, `theme`, `permissions` and friends), and anything else — including a
credential key nobody has thought of yet — is replaced with `<value not shown>`. Permission grants
stay visible because that is what the diff is for, and the routing keys keep scheme, host and first
path segment so you can still see which port you were pointed at.

It is an allowlist rather than a denylist of credential-ish names because the earlier denylist leaked
four times, in four shapes nobody had listed: `sk_live_`, a secret in a URL path, `?auth=`, and
`apiKeyHelper` with an object value. The same filter runs over every place this script echoes
content — the diff, the verify pass, the no-record grep, and the exported-variable lines.

### It restores routing, not credentials

Worth being blunt about, because the incident that prompted the hatch was **not** a routing fault.
Both `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` were set, the gateway rejected whichever one
won, and the export was a single line in a shell rc. No amount of settings surgery reaches that.

So the hatch ends with an environment report: whether both credential variables are set, whether
`ANTHROPIC_BASE_URL` is exported in your *shell* (where no settings file can override it), and the
`file:line` of every `ANTHROPIC_*` assignment in your shell startup files. It prints the locations
and never the values — a recovery tool that echoes a live key into your terminal buffer is not one.
Fixing those lines is yours to do; the hatch will not edit them.

### If there is no record

A hand-edited settings file, or a wiped state directory, and the hatch has nothing to restore
from. It says so, prints the three files worth checking, greps each for the keys, and lists the
timestamped backups beside them — rather than reporting "nothing to do" to somebody whose sessions
are down. Exit status is 3: finished, with something left for a human.

## Troubleshooting

**Every request fails or hangs, and `/context-guru:uninstall` cannot run.** Run this:

```bash
~/.local/state/context-guru/context-guru-reset
```

That is the whole fix. It needs no working Claude session — which is the point, because when routing
is what broke, the skill that would undo it cannot reach a model either. It restores every settings
file this plugin edited, prints what it changed, and ends by naming anything a file restore cannot
fix (a credential variable, an `ANTHROPIC_BASE_URL` exported in your shell). Add `--dry-run` first if
you want the plan without the change. Full detail: [the escape
hatch](#the-escape-hatch-when-claude-code-cannot-fix-it-for-you).

**"Nothing happened after `/context-guru:install`."** The setting applies to a **new** session;
the one you ran it in already has its environment. Start a new session.

Expand Down
Loading