Skip to content

Environment variables

VariableMeaning
GRAPHMIND_DBDatabase path (--db beats it; default ~/.graphmind/graphmind.db)
GRAPHMIND_PAUSE_ON_ERRORScope 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_MSHow 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_ORIGINSExtra browser origins allowed to reach the server, comma-separated; * disables the check. See who is allowed to connect.
GRAPHMIND_RETENTIONoff disables the startup prune entirely
GRAPHMIND_KEEP_RUNSRuns to keep (default 200); --keep beats it
GRAPHMIND_KEEP_DAYSDays to keep (default 30); --days beats it
GRAPHMIND_TELEMETRY0 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_TRACK1 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.
CISet to anything, telemetry is off (unless GRAPHMIND_TELEMETRY=log)
GRAPHMIND_TELEMETRY_URLOverride the telemetry endpoint (used by tests)
GRAPHMIND_HOMEDirectory for the telemetry install id (default ~/.graphmind)
GRAPHMIND_DEMO_AGENT_DIRWhere demo --live finds the demo agent outside a monorepo checkout
GRAPHMIND_VIEWER_DISTServe a different viewer build directory (development)
VariableMeaning
GRAPHMIND_URLIngest endpoint for adapters (default ws://127.0.0.1:4747/ingest)
GRAPHMIND_DISABLED1 disables instrumentation — beats everything, including an explicit enabled: true
GRAPHMIND1 re-enables instrumentation under NODE_ENV=production (disabled there by default)
GRAPHMIND_HIDE_INPUTS1 or true: every node’s input is recorded as "__REDACTED__". See recording less.
GRAPHMIND_HIDE_OUTPUTS1 or true: every node’s output, and the text of every streamed token, is recorded as hidden
GRAPHMIND_HIDE_TOOL_ARGS1 or true: tool nodes’ input only
GRAPHMIND_HIDE_TOOL_RESULTS1 or true: tool nodes’ output only
GRAPHMIND_LOOP_THRESHOLDHow 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_LOOPpause (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_ALLOWComma-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.

Evaluated top to bottom; the first match wins:

ConditionResult
GRAPHMIND_DISABLED=1Disabled. Beats everything, including enabled: true.
enabled option passedAs given.
NODE_ENV=productionDisabled unless GRAPHMIND=1.
OtherwiseEnabled.

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.

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.

SwitchSession optionReplaces
GRAPHMIND_HIDE_INPUTShideInputsinput on every node (prompts, messages, tool arguments, MCP params), and streamed tool-argument text
GRAPHMIND_HIDE_OUTPUTShideOutputsoutput on every node, and the text of every streamed token
GRAPHMIND_HIDE_TOOL_ARGShideToolArgsinput on tool nodes, and streamed tool-argument text
GRAPHMIND_HIDE_TOOL_RESULTShideToolResultsoutput 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: 1 or true, 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: false to undo GRAPHMIND_HIDE_INPUTS=1. An option set to 1, "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 with input/output hidden, deltas: [] and redaction.failed: true. If even its identity fields cannot be read, it is dropped with one warning.
  • node.error is 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, use GRAPHMIND_HIDE_INPUTS and GRAPHMIND_HIDE_OUTPUTS.
  • Under graphmind mcp-proxy: with GRAPHMIND_HIDE_TOOL_RESULTS (or _OUTPUTS), a failed isError: true result is not quoted into the error message. The message says the call failed and that the content is hidden. With GRAPHMIND_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 and key=value pairs 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.fingerprint is also "__REDACTED__" (under GRAPHMIND_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.

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.0 and 1) do not matter. MCP’s per-request _meta object is ignored, because clients put a fresh progressToken in 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 → search is 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 graphmind server, whether or not a viewer tab is open. As with the default pause-on-error, a loop can hold with nobody watching. Set pauseTimeoutMs if 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 this

Configuration. For each field the option wins over the environment variable, which wins over the default:

Option (loopGuard: {…})EnvDefaultMeaning
thresholdGRAPHMIND_LOOP_THRESHOLD3Identical calls in a row that count as a loop; 0 turns it off
modeGRAPHMIND_ON_LOOPpausepause, warn or off
allowNodesGRAPHMIND_LOOP_ALLOWnoneNode 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_cheap in each), where the canonicaliser is interpreted code. For a tool that takes megabytes of arguments, use allowNodes or GRAPHMIND_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-proxy a whole host session is one run. In an app that does not wrap each request in gm.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 before gate 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.

Two sides have to agree. If you move the server, tell the app:

Terminal window
# terminal 1
graphmind --port 4848
# terminal 2
GRAPHMIND_URL=ws://127.0.0.1:4848/ingest node agent.js

Or 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:

Terminal window
claude mcp add graphmind -- npx graphmind-ai mcp --port 4848

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:

Terminal window
# in the container
GRAPHMIND_URL=ws://host.docker.internal:4747/ingest
# on the host
GRAPHMIND_ALLOWED_ORIGINS=http://host.docker.internal:4747 npx graphmind-ai

This 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:

OptionDefaultEnvironment equivalent
urlws://127.0.0.1:4747/ingestGRAPHMIND_URL
enabledkill-switch logicGRAPHMIND_DISABLED / GRAPHMIND
connectTimeoutMs300—
handshakeTimeoutMs1000—
retryIntervalMs10000—
bufferSize5000 (and at most maxBufferBytes, 8 MiB)—
pauseTimeoutMshold forever—
hideInputs / hideOutputs / hideToolArgs / hideToolResultsfalseGRAPHMIND_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