Environment variables
Read by the CLI
Section titled “Read by the CLI”| Variable | Meaning |
|---|---|
GRAPHMIND_DB | Database path (--db beats it; default ~/.graphmind/graphmind.db) |
GRAPHMIND_PAUSE_ON_ERROR | Scope the default error breakpoint: on (default, every node), off, or a node kind (tool, llm, chain, retriever, agent, custom). --pause-on-error beats it. |
GRAPHMIND_ABANDON_GRACE_MS | How long a run whose app disconnected mid-flight waits to reconnect before it is marked abandoned rather than left “running” forever (default 15000). |
GRAPHMIND_ALLOWED_ORIGINS | Extra browser origins allowed to reach the server, comma-separated; * disables the check. See who is allowed to connect. |
GRAPHMIND_RETENTION | off disables the startup prune entirely |
GRAPHMIND_KEEP_RUNS | Runs to keep (default 200); --keep beats it |
GRAPHMIND_KEEP_DAYS | Days to keep (default 30); --days beats it |
GRAPHMIND_TELEMETRY | 0 or false disables telemetry. log prints the exact record to stderr, prefixed [graphmind telemetry] , and sends nothing, even when CI is set. Any other value leaves telemetry on unless CI is set. |
DO_NOT_TRACK | 1 or true (any case, surrounding spaces ignored) disables telemetry. It beats every GRAPHMIND_TELEMETRY value, including 1 and log. Other values such as yes are ignored. |
CI | Set to anything, telemetry is off (unless GRAPHMIND_TELEMETRY=log) |
GRAPHMIND_TELEMETRY_URL | Override the telemetry endpoint (used by tests) |
GRAPHMIND_HOME | Directory for the telemetry install id (default ~/.graphmind) |
GRAPHMIND_DEMO_AGENT_DIR | Where demo --live finds the demo agent outside a monorepo checkout |
GRAPHMIND_VIEWER_DIST | Serve a different viewer build directory (development) |
Read by the instrumented app
Section titled “Read by the instrumented app”| Variable | Meaning |
|---|---|
GRAPHMIND_URL | Ingest endpoint for adapters (default ws://127.0.0.1:4747/ingest) |
GRAPHMIND_DISABLED | 1 disables instrumentation — beats everything, including an explicit enabled: true |
GRAPHMIND | 1 re-enables instrumentation under NODE_ENV=production (disabled there by default) |
GRAPHMIND_HIDE_INPUTS | 1 or true: every node’s input is recorded as "__REDACTED__". See recording less. |
GRAPHMIND_HIDE_OUTPUTS | 1 or true: every node’s output, and the text of every streamed token, is recorded as hidden |
GRAPHMIND_HIDE_TOOL_ARGS | 1 or true: tool nodes’ input only |
GRAPHMIND_HIDE_TOOL_RESULTS | 1 or true: tool nodes’ output only |
GRAPHMIND_LOOP_THRESHOLD | How many identical calls in a row count as a loop. Default 3; 0 turns the loop hold off; anything that is not a whole number ≥ 0 falls back to 3. See loop hold. |
GRAPHMIND_ON_LOOP | pause (default: hold when a debugger is attached, warn when not), warn (never hold, warn once per streak), or off. 0, false and none also mean off; any other value falls back to pause. |
GRAPHMIND_LOOP_ALLOW | Comma-separated node ids or tool names that are never held as a loop, e.g. pollJob,tool:heartbeat |
The GRAPHMIND_HIDE_* and loop variables are read by the TypeScript client (so by every TS
adapter and by graphmind mcp-proxy), by the Python SDK and by the Ruby gem, with the same
names and meanings. Set them in the environment of the instrumented app, or of
graphmind mcp-proxy. Setting them on graphmind serve does nothing.
Kill-switch precedence
Section titled “Kill-switch precedence”Evaluated top to bottom; the first match wins:
| Condition | Result |
|---|---|
GRAPHMIND_DISABLED=1 | Disabled. Beats everything, including enabled: true. |
enabled option passed | As given. |
NODE_ENV=production | Disabled unless GRAPHMIND=1. |
| Otherwise | Enabled. |
Python has no NODE_ENV, so it applies the same rule to the first variable that is set out of
GRAPHMIND_ENV, ENVIRONMENT, APP_ENV, PYTHON_ENV, ENV, DJANGO_ENV, FLASK_ENV,
NODE_ENV, counting production / prod (case-insensitive) as production. It is a boring
documented list rather than a heuristic on purpose — a debugger that turns itself off for
surprising reasons is worse than one you have to switch on.
A disabled session no-ops everything: no sockets, no buffering, no warnings, and the adapter’s
wrapClient / wrapModel / wrapTools are identity functions. gm.run still executes your
function and still hands it a working context (ids + abort signal), so your code never has to
branch on whether GraphMind is on.
Recording less
Section titled “Recording less”Four switches keep values out of the recording altogether. The instrumented app replaces the
value with the string "__REDACTED__" before the event is buffered or sent. Nothing downstream
ever sees the original: not a debugger that attaches later and gets the buffered events, not
the database, not graphmind record, not the read-only MCP tools.
| Switch | Session option | Replaces |
|---|---|---|
GRAPHMIND_HIDE_INPUTS | hideInputs | input on every node (prompts, messages, tool arguments, MCP params), and streamed tool-argument text |
GRAPHMIND_HIDE_OUTPUTS | hideOutputs | output on every node, and the text of every streamed token |
GRAPHMIND_HIDE_TOOL_ARGS | hideToolArgs | input on tool nodes, and streamed tool-argument text |
GRAPHMIND_HIDE_TOOL_RESULTS | hideToolResults | output on tool nodes, and a tool node’s streamed text |
Checked by running each one against a real server, 2026-09-14:
GRAPHMIND_HIDE_OUTPUTS=true node.token llm:step deltas=[{"t":"text","v":"","chars":5}] redaction={"count":1,"keys":["deltas"]} node.finished llm:step output="__REDACTED__" redaction={"count":1,"keys":["output"]}GRAPHMIND_HIDE_TOOL_RESULTS=true node.finished llm:step output="hello" node.finished tool:search output="__REDACTED__" redaction={"count":1,"keys":["output"]}- Values:
1ortrue, in any case, with surrounding spaces ignored. Anything else is off. - Env and option: either one turning a switch on turns it on. An app cannot pass
hideInputs: falseto undoGRAPHMIND_HIDE_INPUTS=1. An option set to1,"1"or"true"counts as on, so a config file that spells it as a string does not quietly record. - What survives: node names, kinds, ids, timings, token counts, the number of streamed
deltas and each delta’s length (
chars). The graph still reads; the contents do not. - A payload that cannot be read safely is not sent raw. If a switch is on and reading the
payload fails (a throwing getter or Proxy, a malformed
deltas), the event is sent withinput/outputhidden,deltas: []andredaction.failed: true. If even its identity fields cannot be read, it is dropped with one warning. node.erroris never redacted. An error message or stack can repeat the data it failed on, so these switches do not guarantee that no value is recorded.- The tool-only switches hide only the tool node’s own input and output. In an agent loop
the same arguments appear in the model’s output (
tool_use), and the same results appear in the next model request (tool_result). To keep them out of the recording, useGRAPHMIND_HIDE_INPUTSandGRAPHMIND_HIDE_OUTPUTS. - Under
graphmind mcp-proxy: withGRAPHMIND_HIDE_TOOL_RESULTS(or_OUTPUTS), a failedisError: trueresult is not quoted into the error message. The message says the call failed and that the content is hidden. WithGRAPHMIND_HIDE_INPUTS, the server’s command-line arguments are left out of the run metadata. Its label keeps only the command’s name and the file names of path arguments; flags, their values, URLs andkey=valuepairs are dropped. JSON-RPC error messages are still recorded. - The viewer shows a “hidden by …” chip where the value would be, and refuses to inject a value that still contains the placeholder. See the viewer.
- The loop fingerprint follows
hideInputs. When inputs are hidden,exec.paused.loop.fingerprintis also"__REDACTED__"(underGRAPHMIND_HIDE_TOOL_ARGS, for tool nodes). A digest of a low-entropy input can be reversed by guessing.
For exports, a separate mechanism removes values under secret-looking keys by default:
graphmind record.
Loop hold
Section titled “Loop hold”While a debugger is attached, GraphMind holds an agent that calls the same tool with the same
arguments again and again. The third call in a row is stopped at its before gate, reason
loop, and the card reads Loop: 3× searchFlights with identical arguments. Continue, Retry,
Inject and Abort work as they do at any gate. Loop holds obey pauseTimeoutMs and are released
if the debugger disconnects.
- Identical means: same node, same arguments after canonicalisation. Key order, whitespace
and number spelling (
1.0and1) do not matter. MCP’s per-request_metaobject is ignored, because clients put a freshprogressTokenin it on every call. Pagination keys (page,cursor,offset) are not ignored, so page 3 of a listing is a different call. - In a row means back-to-back within one run. Any other watched call in between, whether
a different tool or the same tool with different arguments, starts the count again. LLM
steps are not watched, so
model → search → model → search → model → searchis still three in a row. An agent alternating between two tools (search, read, search, read) is not held. - Watched kinds: tool calls only. LLM steps are not watched unless you set
kinds. - Not attached (or
GRAPHMIND_ON_LOOP=warn): nothing is held. One warning per streak is logged; it names the tool, never its arguments. The count keeps running, so a debugger that attaches later holds the next identical call. - Attached means connected to a running
graphmindserver, whether or not a viewer tab is open. As with the default pause-on-error, a loop can hold with nobody watching. SetpauseTimeoutMsif that matters to you.
Checked against a real server (GRAPHMIND_ON_LOOP=warn) and with no server at all:
[graphmind] possible loop: search (tool:search) was called 3× in a row with identical arguments; GRAPHMIND_ON_LOOP=warn, so it is not being held. Polling on purpose? add it to loopGuard.allowNodes; GRAPHMIND_ON_LOOP=off silences this[graphmind] possible loop: search (tool:search) was called 3× in a row with identical arguments; no debugger is attached to hold it (start `npx graphmind-ai` to pause it there). Polling on purpose? add it to loopGuard.allowNodes; GRAPHMIND_ON_LOOP=off silences thisConfiguration. For each field the option wins over the environment variable, which wins over the default:
Option (loopGuard: {…}) | Env | Default | Meaning |
|---|---|---|---|
threshold | GRAPHMIND_LOOP_THRESHOLD | 3 | Identical calls in a row that count as a loop; 0 turns it off |
mode | GRAPHMIND_ON_LOOP | pause | pause, warn or off |
allowNodes | GRAPHMIND_LOOP_ALLOW | none | Node ids (tool:pollJob) or tool names (pollJob) never held. Use it for tools that poll on purpose. |
ignoreKeys | — | ['_meta'] | Keys removed at every depth before comparing. Your list replaces the default. |
kinds | — | ['tool'] | Node kinds watched. Your list replaces the default. |
loopGuard: false turns it off in code. Python spells the option
loop_guard={"threshold": 5, "allow_nodes": [...]} and Ruby loop_guard: { allow_nodes: [...] }.
Under graphmind mcp-proxy, which takes no session options, the environment variables are the
only way to configure it.
Costs and limits:
- Fingerprinting cost: each watched tool’s start pays to fingerprint its arguments, even
when detached, and the cost grows with the size of the arguments. The tests bound it for a
1 KB input: under 0.05 ms on average in TypeScript (
packages/client/test/loop-guard.test.ts), under 0.5 ms in Python and Ruby (test_fingerprinting_a_1kb_input_is_cheapin each), where the canonicaliser is interpreted code. For a tool that takes megabytes of arguments, useallowNodesorGRAPHMIND_ON_LOOP=off. - Identical calls in parallel also count. Their starts are back-to-back, so once the threshold is reached each further identical call can be held.
- One run can span requests. Under
graphmind mcp-proxya whole host session is one run. In an app that does not wrap each request ingm.run, every request shares one implicit run. Either way, the same call three times in a row, with no other tool call between, is held even when the calls came from separate requests. - A poll with byte-identical arguments looks exactly like a loop. Use
allowNodes. - Only where a
beforegate exists. Provider-executed and other ungated tools can warn but cannot be held.
exec.paused carries reason: 'loop' and loop: { repeats, firstSeq, lastSeq, fingerprint };
see the wire protocol. To try it:
pnpm --filter demo-agent start -- --loop from a checkout, with npx graphmind-ai running.
Non-standard ports
Section titled “Non-standard ports”Two sides have to agree. If you move the server, tell the app:
# terminal 1graphmind --port 4848
# terminal 2GRAPHMIND_URL=ws://127.0.0.1:4848/ingest node agent.jsOr set it in code, which wins over the variable:
const gm = graphmind({ app: 'my-agent', url: 'ws://127.0.0.1:4848/ingest' });If you also use MCP, pass the port there too so its deep links point at the right viewer:
claude mcp add graphmind -- npx graphmind-ai mcp --port 4848Docker & remote containers
Section titled “Docker & remote containers”The adapter dials 127.0.0.1 inside its own network namespace. The simplest setup is to run
npx graphmind-ai in the same container as the agent and forward port 4747 to your browser.
See remote development, which also covers devcontainers,
Codespaces, WSL 2 and SSH.
To keep the server on the host instead, point the adapter at it and allow that hostname on
the server. Without the second line the connection is refused with 403, because
host.docker.internal is not a loopback name:
# in the containerGRAPHMIND_URL=ws://host.docker.internal:4747/ingest# on the hostGRAPHMIND_ALLOWED_ORIGINS=http://host.docker.internal:4747 npx graphmind-aiThis works where host.docker.internal reaches the host’s loopback (Docker Desktop). On Linux,
--add-host=host.docker.internal:host-gateway points at the bridge address, where a server
bound to 127.0.0.1 is not listening. There, run the server in the container.
Session options that shadow the environment
Section titled “Session options that shadow the environment”Options passed to graphmind({ … }) take precedence over the variables:
| Option | Default | Environment equivalent |
|---|---|---|
url | ws://127.0.0.1:4747/ingest | GRAPHMIND_URL |
enabled | kill-switch logic | GRAPHMIND_DISABLED / GRAPHMIND |
connectTimeoutMs | 300 | — |
handshakeTimeoutMs | 1000 | — |
retryIntervalMs | 10000 | — |
bufferSize | 5000 (and at most maxBufferBytes, 8 MiB) | — |
pauseTimeoutMs | hold forever | — |
hideInputs / hideOutputs / hideToolArgs / hideToolResults | false | GRAPHMIND_HIDE_*. Not shadowed: either one turning a switch on wins. |
loopGuard | { threshold: 3, mode: 'pause', … } | GRAPHMIND_LOOP_THRESHOLD, GRAPHMIND_ON_LOOP, GRAPHMIND_LOOP_ALLOW, field by field |