Connect to the IBM service¶
There is a context-guru instance running for IBM engineers. You do not build anything and you do not run a proxy — you register once, get a token, and add two settings to Claude Code. Your own provider key stays yours and is forwarded unchanged.
your agent ──▶ https://contextguru.vpc.cloud9.ibm.com ──▶ the model gateway
(rewrites `messages`, then forwards) (your own credential)
│
└──▶ your own dashboard: savings, sessions, before/after diffs
| Host | contextguru.vpc.cloud9.ibm.com, HTTPS on 443 only |
| Reachable from | IBM-internal networks (9.0.0.0/8) |
| Credential | one service-issued token, cg_live_ + 26 characters |
| Your provider key | stays yours — your agent keeps sending it, and the proxy forwards it upstream unchanged |
| Cost control | none needed: every account's traffic is billed to that account's own provider credential |
| Transcript capture | your account consents on registration — what that means, and the off switch |
| Default pipeline | [format, dedup, toon, cmdfilter, searchfold, textclean, extract, cachesplit, toolfilter] (the house preset), mode: sync |
The default pipeline is fully deterministic — no cheap-model calls anywhere in it. That is why it is the default on a shared box: it adds no upstream spend, contends for no shared model budget, and puts near-zero latency on your agent turn.
There is no port 80, on purpose
A mistyped http://contextguru.vpc.cloud9.ibm.com fails to connect, and that is the
designed behaviour rather than a gap. Every request carries your cg_live_… token AND
your own provider key in headers; a 301 to https:// arrives after the client has
already put both on the wire in cleartext, and a redirect cannot retract a credential
that has been sent. Failing loudly is the only outcome that keeps them off the network.
1. Register¶
Open https://contextguru.vpc.cloud9.ibm.com/dashboard/, register with your IBM email address, a password of at least 8 characters, and a label for the token. We mail a 6-digit code to that address; entering it within 5 minutes is what creates the account. Signing in later is that same password plus a fresh mailed code.
The token is shown once, on the screen straight after the code, with a copy button and
the setup steps below it already filled in with your token. The server keeps only
sha256(token) and its first 8 characters (for display and revocation), so there is no
code path that can print it back to you and a lost token has to be reissued, not
recovered. Copy it somewhere safe before you leave that screen.
If registration is refused
Self-registration has three modes, and the operator picks one. A 403 means it is
closed (ask the operator to reissue you an account) or invite (ask for the invite
code and enter it in the form). See
Choose how accounts are created.
2. Make sure your machine trusts the certificate¶
The certificate is issued by the IBM INTERNAL INTERMEDIATE CA, chaining to the IBM Internal Root CA. This is the most common first-run failure, so check before anything else — and check once per runtime, because they do not share a trust store:
# The OS trust store (curl, Python, Go).
curl -sS -o /dev/null -w '%{http_code}\n' https://contextguru.vpc.cloud9.ibm.com/healthz
# Node's own trust store (Bob, Gemini CLI, anything on `fetch`). NOT the same answer.
node -e 'fetch("https://contextguru.vpc.cloud9.ibm.com/healthz").then(r=>r.text()).then(console.log).catch(e=>console.log("FAIL",e.cause?.code))'
Both must print a success line: 200 from curl, ok from Node.
curl passing does not mean Node passes — this is the trap
Node ships its own bundled CA list and ignores the operating system's entirely. So
on an IBM-managed machine, whose OS trust store does have the IBM root, the curl
check returns 200 while every Node agent still fails. Bob reports that as:
…with no request in the service's log, because the connection never got that far.
The message names nothing, so it reads as "the proxy is down". It is not: the real error
is UNABLE_TO_GET_ISSUER_CERT_LOCALLY, visible only from the node -e check above.
Observed on a machine where curl returned 200 (context-guru 2026-08-17).
The fix is NODE_EXTRA_CA_CERTS — see the One tool only tab below. Installing the
root system-wide does not help Node on its own; that variable is what makes Node
read it.
A certificate-verification error from curl means the OS trust store is missing the IBM
root — usually reported as a self-signed certificate in the chain, which is misleading: the
chain is fine (the service serves the intermediate), your machine simply does not know the
root.
Install the root CA system-wide:
# Node ignores the OS trust store, so this one is REQUIRED even on a machine where
# curl already works. It also accepts the OS bundle itself, which saves finding the
# single PEM: /etc/pki/tls/certs/ca-bundle.crt (RHEL) or
# /etc/ssl/certs/ca-certificates.crt (Debian).
export NODE_EXTRA_CA_CERTS=/path/to/ibm-internal-root-ca.pem # Claude Code, Bob (Node)
export SSL_CERT_FILE=/path/to/ibm-internal-root-ca.pem # Python / OpenSSL
export REQUESTS_CA_BUNDLE=/path/to/ibm-internal-root-ca.pem # python-requests
export CURL_CA_BUNDLE=/path/to/ibm-internal-root-ca.pem # curl
Per-process, so it fixes one agent and leaves the machine's trust store alone. Useful in a container you do not own.
Where the root CA PEM comes from is not in this repo
The certificate is distributed through IBM's own internal CA channels, not by
context-guru — this page cannot tell you a download URL it can verify. Ask the operator;
the deployment keeps a readable copy at /etc/context-guru/ibm-internal-root-ca.pem,
which is the path
deploy/service/tls-smoke.sh
defaults to.
3. Point Claude Code at it¶
Four steps. Go through them in order and do nothing else.
1. Open your Claude Code settings file.
No such file? Create it — an empty file is fine.
2. Put this in it, with your own token in place of cg_live_YOUR_TOKEN_HERE. This is
the whole file, not a diff:
{
"env": {
"ANTHROPIC_BASE_URL": "https://contextguru.vpc.cloud9.ibm.com/anthropic",
"ANTHROPIC_CUSTOM_HEADERS": "x-context-guru-token: cg_live_YOUR_TOKEN_HERE"
}
}
Already have an env block? Add just those two lines inside it and leave every other key
alone.
3. Keep your own key exactly where it is. ANTHROPIC_API_KEY (or
ANTHROPIC_AUTH_TOKEN) stays yours: we forward it, so your traffic is billed to you, and a
request with no provider credential of its own answers 401. The context-guru token
travels in its own header, so it never competes for that slot.
4. Restart Claude Code and ask it anything. Your request appears on the dashboard Overview within a second. Zero requests after a full turn means the traffic never arrived — see Is it actually on?.
Put it in that file, not in your shell
An env block in ~/.claude/settings.json silently overrides an exported
ANTHROPIC_BASE_URL. The failure looks exactly like success: Claude Code answers
normally, nothing errors, and the only symptom is an empty dashboard and zero savings.
This has already caught us on this deployment — so if you exported the variable and see
nothing, look in that file first.
Other agents, and where else the token is accepted
One token, three dialects. The path carries the dialect; your account's settings decide
which upstream each dialect goes to. Neither of these reads ~/.claude/settings.json, so
they are still shell variables:
# OpenAI-dialect tools — OPENAI_API_KEY stays your key; send the header
# x-context-guru-token: cg_live_YOUR_TOKEN_HERE
export OPENAI_BASE_URL=https://contextguru.vpc.cloud9.ibm.com/openai/v1
# Bob — your Bob key stays your own, and Bob can send no header of ours, so bind
# that key to your account once on the Settings tab (sha256 only; never stored)
export BOB_GATEWAY_URL=https://contextguru.vpc.cloud9.ibm.com # bobshell 2.x
export CUSTOM_BASE_URL=https://contextguru.vpc.cloud9.ibm.com # older builds
export NODE_EXTRA_CA_CERTS=/etc/pki/tls/certs/ca-bundle.crt # step 2 — Bob is Node
Which base-URL variable is version-dependent, and guessing wrong is silent — Bob
just talks to its own default gateway and nothing shows up in your dashboard. Check with
bob --version: the 2.x bundle reads BOB_GATEWAY_URL and contains no reference to
CUSTOM_BASE_URL at all; older builds read CUSTOM_BASE_URL. Exporting both is harmless.
Then bind the key, in the dashboard — Settings → Bound agent keys → paste the key
your Bob sends (BOB_API_KEY, or the older BOBSHELL_API_KEY) → Bind this key. Only
its sha256 is kept. Rebind whenever you rotate the key. There is no need to copy a cookie
into a curl line: your browser is already signed in, which is the whole reason the field
is there.
Bob's key must be a real Bob credential, because the service forwards it upstream unchanged — you are billed by IBM, not by us. And it must be an API key, not SSO: the identity here is the digest of a stable credential, and an SSO bearer token is reissued on every login, so it would need rebinding each time.
The token is read from x-context-guru-token first. It is still accepted in
Authorization, x-api-key or x-goog-api-key for tools that have nowhere else to put
it — recognised by its cg_live_ shape, and scrubbed out before the request is forwarded —
but a slot holding the token cannot also hold your provider key, so prefer the header.
Turn it on for one session only¶
Nothing below edits a config file. Each recipe affects exactly the command you run, so another terminal, another repo and another agent are untouched.
Claude Code¶
# One command, one session. Your own key stays in ANTHROPIC_API_KEY.
ANTHROPIC_BASE_URL=https://contextguru.vpc.cloud9.ibm.com/anthropic \
ANTHROPIC_CUSTOM_HEADERS='x-context-guru-token: cg_live_YOUR_TOKEN_HERE' \
claude
If ~/.claude/settings.json has an env block, the line above does nothing — the same
override as in step 2, and the settings file wins. --settings takes a JSON string as well
as a path, read as additional settings for this invocation only, so it beats the global
file without touching it:
claude --settings '{"env":{
"ANTHROPIC_BASE_URL":"https://contextguru.vpc.cloud9.ibm.com/anthropic",
"ANTHROPIC_CUSTOM_HEADERS":"x-context-guru-token: cg_live_YOUR_TOKEN_HERE"}}'
Verified on Claude Code 2.1.215: with a global env block present, the --settings form
sent POST /v1/messages to the URL named there, and the exported variable alone did not.
Bob¶
BOB_GATEWAY_URL=https://contextguru.vpc.cloud9.ibm.com \
CUSTOM_BASE_URL=https://contextguru.vpc.cloud9.ibm.com \
NODE_EXTRA_CA_CERTS=/etc/pki/tls/certs/ca-bundle.crt \
bob "your task"
Bob keeps its own key. Because it can carry no header of ours, it is identified by the sha256 of that key — bind it once on the Settings tab, and rebind whenever you rotate it.
NODE_EXTRA_CA_CERTS is needed because Bob is a Node program and Node reads its own CA
list rather than the system store; without it the session ends in Request failed after 6
attempts: fetch failed — see step 2.
Recent Bob on recent Node no longer needs it: bobshell 2.0.1 merges the system store
via tls.getCACertificates, which exists from Node v22.15, and prints unable to auto
setup system certificates when it does not. Setting it anyway costs nothing and covers
both cases.
Bob's base URL is the host; Bob appends its own /inference/… and /admin/… paths, and
the proxy passes its control-plane calls through verbatim so the CLI still boots.
Or one shell, then any number of commands¶
cg-on() { export ANTHROPIC_BASE_URL=https://contextguru.vpc.cloud9.ibm.com/anthropic \
ANTHROPIC_CUSTOM_HEADERS="x-context-guru-token: $CG_TOKEN" \
BOB_GATEWAY_URL=https://contextguru.vpc.cloud9.ibm.com \
CUSTOM_BASE_URL=https://contextguru.vpc.cloud9.ibm.com; }
cg-off() { unset ANTHROPIC_BASE_URL ANTHROPIC_CUSTOM_HEADERS BOB_GATEWAY_URL CUSTOM_BASE_URL; }
Keep CG_TOKEN in your own secret store, not in .bashrc. These are a convenience over
the raw commands above, not a different mechanism — and cg-on does not help Claude Code on
a machine whose settings.json sets ANTHROPIC_BASE_URL.
Turn it off for one session only¶
# Claude Code — straight to whatever provider your normal config uses.
env -u ANTHROPIC_BASE_URL -u ANTHROPIC_CUSTOM_HEADERS claude
# Bob — back to its own endpoint.
env -u BOB_GATEWAY_URL -u CUSTOM_BASE_URL bob "your task"
env -u only removes an environment variable. If Claude Code is routed through the
service by your settings.json rather than by an export, unsetting the variable changes
nothing — point it back at the provider explicitly for the one invocation instead:
Keep the metrics, skip the compaction¶
x-context-guru-bypass: true skips the pipeline for that request only: the body is
forwarded byte-identical, no component runs, no expand tool is injected — and the request is
still recorded, so it appears in your dashboard under the bypassed reason and feeds
upstream_ms_avg_bypassed, the latency baseline for a with/without comparison.
Reach for it when you are debugging whether context-guru is implicated in a problem at all. It is a sharper instrument than switching the base URL off, because the request still goes through the same host, the same TLS, the same nginx and the same account — so if the problem survives a bypass, compaction was never the cause.
# Claude Code — verified on 2.1.215: these headers reach the wire. Several pairs are
# newline-separated, which is how the token and the bypass travel together.
ANTHROPIC_CUSTOM_HEADERS='x-context-guru-token: cg_live_YOUR_TOKEN_HERE
x-context-guru-bypass: true' \
ANTHROPIC_BASE_URL=https://contextguru.vpc.cloud9.ibm.com/anthropic \
claude
No equivalent for Bob
Bob 1.0.6 exposes no environment variable for extra request headers — its client
builds Content-Type, User-Agent, Authorization, x-instance-id and x-team-id
itself, its headers setting applies to MCP servers only, and the header-ish knobs in
its bundle are CUSTOM_BASE_URL, CUSTOM_TIMEOUT and the BOBSHELL_* auth set. That
is also why Bob is identified by its key digest rather than a token header. So for Bob,
use
turn it off for one session instead, and accept
that you lose the metrics row for those requests.
The proxy also bypasses one thing on its own: the agent's own compaction request.
Compacting the request that asks for a summary would destroy the content the summary is
meant to carry verbatim, and that loss is unrecoverable once the summary replaces the
transcript. It shows up in /stats as components.bypass.gates.agent_compaction rather
than happening silently.
Measure without modifying — observe mode¶
observe forwards every request untouched, byte for byte, and measures off-path what
this pipeline would have saved. It is the safe way to try a configuration: read the
potential_* numbers, then switch the same config to sync. See
Operating modes.
Mode is per account, not per session
There is no header and no environment variable that switches mode for one session — the
only per-request overrides are x-context-guru-session, x-context-guru-bypass and
x-context-guru-pipeline. Changing to observe changes it for every session you
run until you change it back, and it also rebuilds your pipeline and store, which
discards the frozen compaction decisions your current sessions are replaying.
The button is on the dashboard's Settings page, which is the way to do this. If you need it scripted, the control plane is cookie-authenticated on purpose — a proxy token buys inference, it does not administer the account — so it is two calls:
jar=$(mktemp)
printf '{"token":"%s"}' "$CG_TOKEN" \
| curl -sS -X POST https://contextguru.vpc.cloud9.ibm.com/api/login \
-H 'Content-Type: application/json' --data @- -c "$jar" -o /dev/null
# Read what you are running now: `effective_config_yaml`, and `config_inherited`.
curl -sS -b "$jar" https://contextguru.vpc.cloud9.ibm.com/api/me | python3 -m json.tool
# Store that same document with mode: observe.
curl -sS -b "$jar" -X PUT https://contextguru.vpc.cloud9.ibm.com/api/me \
-H 'Content-Type: application/json' \
-d '{"config_yaml":"pipeline: [format, toon, dedup, failed_run, cmdfilter, extract, cachesplit]\ncomponents:\n extract:\n min_tokens: 400\nmode: observe\n"}'
The token travels in a request body, not a URL, so it stays out of nginx's access log,
and --data @- keeps it off the command line and out of ps(1).
Storing a config opts you out of improvements to the default
A new account tracks the server default — an empty stored document, resolved live on every request — so when the operator improves the default you get it on your next turn. Saving your own document (including just to flip the mode) stops that until you press Follow the server default again. See Tenants track the default.
Is it actually on?¶
Because the silent-override failure looks identical to success, the honest test is a negative one: point the client somewhere unroutable and confirm it fails.
ANTHROPIC_BASE_URL=https://127.0.0.1:1/nope ANTHROPIC_AUTH_TOKEN=bogus \
claude -p 'say PONG' --max-turns 1 # must FAIL
If that prints PONG, your environment variable is being ignored — something else is
choosing the base URL, almost always an env block in ~/.claude/settings.json:
Then use the --settings form above. Full diagnosis and both fixes:
Use with Claude Code.
Then verify positively, which is the only proof that counts: open the dashboard and watch your own request count move. Zero requests after a full agent turn means the traffic never arrived, whatever your shell says.
When it says no¶
| Status | What happened |
|---|---|
| 401 | No token (or an unknown/revoked one), or no provider credential of your own. The service never falls back to somebody else's key. Nothing is treated as an anonymous account. |
| 403 | The account is disabled, or self-registration is closed. |
| 429 | A per-tenant rate or in-flight limit. This one is worth retrying. |
| 502 | No upstream configured for that route, or the provider failed. The operator's problem. |
connection refused on http:// |
You used port 80. There deliberately isn't one — see the warning at the top of this page. |
fetch failed (Bob, or any Node agent), and nothing in the service's log |
Node does not trust the IBM root CA. The request never left your machine, which is why there is nothing to see on our side. Set NODE_EXTRA_CA_CERTS — step 2. A passing curl does not rule this out. |
| 401 "this provider key is not bound to an account" (Bob) | Expected until you bind. Bob can send no header of ours, so it is identified by the sha256 of its own API key: Settings → Bound agent keys → paste it → Bind this key. Rebind after you rotate the key. |
| Bob works, but nothing appears in your dashboard | Your Bob never reached us — the base-URL variable is version-dependent. bobshell 2.x reads BOB_GATEWAY_URL; older builds read CUSTOM_BASE_URL. Export both, then check bob --version. |
"no context-guru token; send it in x-context-guru-token" from /api/me/… |
That route authenticates with your browser session, not the token — the message names the agent header because the same wording serves the proxy routes. Use the dashboard instead of curl; a cg_live_ token in a cg_dash cookie is not a session and fails exactly this way. |
Three things worth knowing before you rely on it¶
Your account consents to transcript capture the moment it is created. Two independent switches decide whether your message content is stored, and only one of them starts closed: the operator's service-wide switch, and your account's own consent — which registration turns on for you. So if the operator has capture enabled, then from your very first request the before/after text of your messages — agent output, tool results, source code — is written to the service's database, scrubbed of known credential shapes and capped at 16 KB per message. It is what makes the diff view work, and two parties can read it: your own account, and the service manager. The scrubber is a pattern denylist, so treat this as storage you have agreed to, not a guarantee.
Check which state you are in, and turn it off if you do not want it. Open a request in the
dashboard: content_captured is the effective answer for your account, and
capture_blocked_by names whichever switch is closed ("operator", "tenant", or "" when
nothing is blocking and your content is being stored). To turn it off for yourself, clear the
transcript-capture consent on the Settings tab — metrics and savings keep working, you lose
the diff view. An operator turns it off for everyone with DASHBOARD_CONTENT=false. Either
switch alone stops the writes, neither is retroactive in either direction, and a proxy you run
yourself from this repository ships with the operator's switch off.
It fails open. Any component error or panic reverts that component only, and the original request is always forwarded as a valid fallback. Compaction going wrong costs you savings, not a turn.
The box is a single point of failure for everyone's agent. That is the real cost of the design, and it is stated here rather than left to be discovered during an outage. The escape hatch is the one above: unset the base URL and your agent goes straight to the provider.
See also: Use with Claude Code · Routes & headers · Dashboard · Running the service