Skip to content

Latest commit

 

History

History
349 lines (274 loc) · 19.3 KB

File metadata and controls

349 lines (274 loc) · 19.3 KB

Network egress inventory

Every outbound destination ClawMetry can contact, what it sends, when, and how to turn it off.

This exists because "does this tool send our source code anywhere?" is the first question in any review of a tool that reads developer machines, and the only useful answer is a complete list someone can verify on the wire. If you find traffic to a host that is not in this table, that is a bug. Please report it (see SECURITY.md).

The tables below were rebuilt from a wire capture of clawmetry 0.12.802 on 2026-09-03, not from reading comments. The capture method is at the end so you can repeat it in five minutes without trusting this page.

Verify it yourself. These commands should produce no unexpected hosts:

# Every absolute http(s) asset reference in a served page (expects: none)
python3 scripts/verify_no_external_assets.py

# Every vendored JS bundle, byte-compared to its published npm release
python3 scripts/verify_vendor.py

# The full-suppression path, exercised in tests
python3 -m pytest tests/test_egress_suppression.py tests/test_e2e_invariants.py tests/test_data_boundary_contract.py

The short version

Deployment Contacts ClawMetry servers? Contacts any third party?
Self-hosted (SELF_HOSTED=true) No No
Air-gapped (CLAWMETRY_OFFLINE=1) No No
Local only / cloud sync off (no clawmetry connect, clawmetry onboard --local, or CLAWMETRY_NO_CLOUD=1) One install ping, plus a field-failure report if the daemon stops working PyPI version check
Managed cloud (clawmetry connect) Yes: sealed content plus plaintext metadata (table below) PyPI version check
Hosted ingest push (a ClawMetry Cloud ingest key, OTLP/JSON) Yes: the pushed telemetry, which is not sealed and is readable by ClawMetry, because the sender holds no encryption key None added

Data-boundary contract version: 1 (2026-09-14). Cloud sync off is not offline: it stops uploads and nothing else. DO_NOT_TRACK=1 stops the install ping and the failure report but not the PyPI check; CLAWMETRY_OFFLINE=1 stops every discretionary request. Public pages that describe these modes must agree with this table; tests/test_data_boundary_contract.py pins the behaviour it describes.

No ClawMetry deployment loads a CDN, font, analytics script, error tracker or tracking pixel. Web assets are vendored into the package and served from the local process; CI fails on any absolute http(s) asset reference in a served page.

Other optional features can contact a model provider with your own API key. Two are off until you turn them on: transcript quality scoring (CLAWMETRY_EVALS_ENABLED=1) and the Claude rate-limit probe (CLAWMETRY_CLAUDE_LIMIT_PROBE=1). One is on when an ANTHROPIC_API_KEY is present: the alert narrator, which sends the alert's own one-line message and rule id, never session content (CLAWMETRY_NARRATOR_ENABLED=0 to stop it).

Destinations belonging to your agents (api.anthropic.com, api.openai.com and similar) also appear as strings used to attribute costs and parse pricing. Outside the optional inference features described here, ClawMetry observes those calls rather than making them.

The local assessment privacy component adds no network destination and enables no inference. It provides mandatory masking and a consent/egress check for future assessment callers. It does not replace the redaction behavior of the existing optional model features above. A managed assessment integration must disclose its processor, allowed evidence and retention policy separately before sending a sanitized request.

Optional dashboard Assistant

Sending a question in Assistant explicitly invokes the selected AI connection. The planner receives the question, recent conversation and table/column schema. The answer step also receives bounded, credential-scrubbed query-result previews. Raw event payloads, raw memory bodies and file reads are excluded from its SQL surface. The page discloses this inference boundary before sending. Conversations and saved panel definitions remain in local DuckDB.

The supported connections are the installed Claude CLI (which contacts its configured Anthropic service with tools and project customization disabled), a request-scoped Anthropic API key (api.anthropic.com/v1/messages), and an optional authenticated Builder connection (build.clawmetry.com/api/assistant/*). Builder availability and balance checks require a configured account key; no anonymous balance or credit grant is assumed. Starting checkout requires an explicit top-up action. The API key typed into the dashboard is never persisted. Offline and self-hosted suppression also disable this external inference path.

These are user-requested inference calls, separate from encrypted cloud sync. Do not interpret the Assistant's data notice as a claim that an AI provider cannot read the prompt or the selected query results it processes.


What the server can read, and what it cannot

After clawmetry connect, two kinds of data leave the machine.

Sealed. Encrypted on the node with AES-256-GCM (96-bit random nonce per blob) under a key the server never receives. Large blobs are gzip-compressed before encryption (about 4x smaller; ciphertext itself does not compress), but only once the server has said every reader it serves can inflate (heartbeat caps.blob_gzip); CLAWMETRY_BLOB_GZIP=0 turns it off. The server stores the blob; the browser decrypts it. A node with no key skips these uploads rather than sending them in the clear (content_egress_permitted in clawmetry/sync.py, enforced by tests/test_e2e_invariants.py).

  • Prompts, replies, thinking, tool inputs and tool outputs
  • Session titles (the plaintext session row carries only the session id)
  • Working directory and workspace paths
  • Gateway log lines and log records
  • Memory and instruction files: MEMORY.md, SOUL.md, AGENTS.md, project memory notes, skills and plugin SKILL.md files. Runtime config files (settings.json, openclaw.json, MCP manifests) are catalogued locally but never pushed, sealed or not, because they tend to hold tokens
  • Local IP, CPU and RAM details, the security posture scan, the audit log
  • Cron prompt text, alert rule bodies, the approval queue
  • Everything the cloud pulls on demand: transcript pages, traces, search results

Plaintext. JSON over TLS that the server parses so it can list, sort and bill. Readable by ClawMetry and by anyone who obtains the server's data.

  • Machine hostname, on almost every request, as node_id and the X-Node-Id header
  • The account key, as the X-Api-Key header on every request (never in a URL). The cloud stores the account key alongside a SHA-256 hash so it can hand the key back at sign-in. It authenticates; it cannot decrypt sealed content
  • Per session: id, runtime and model names, status, start and last-active times, token counts, cost in USD, cache read/write split, cache savings, tool error rate, tool call count, message count, surface (terminal/editor)
  • Heartbeat: names of tools used today with counts, the last tool used, detector one-liners such as Bash failed 5 times or 27 files changed in one stretch, installed runtimes with session counts and last-active time, local Ollama model names, billing plan labels, daily activity counters, a random install id, the local DuckDB size, and the names of cache keys
  • Cron job names, cron expressions and models (prompt text, watched command and last error are stripped)
  • Alert rule names and counter-style summaries when a rule fires
  • Trial days left and plan name, at most once a day on a trial

If the plaintext column is more than your review allows, run self-hosted or local-only. There is no "metadata-minimal" cloud mode yet.


Deployment modes in detail

Self-hosted (ClawMetry Enterprise)

Set SELF_HOSTED=true on the server. Node daemons point at it with CLAWMETRY_ENDPOINT=https://clawmetry.your-company.internal.

Outbound calls: none. Node daemons talk only to your server. The server talks to nothing. Telemetry, funnel analytics, public-IP lookup and update checks are all suppressed. See the suppression gate below.

One optional exception, off by default: setting CLAWMETRY_LICENSE_PING=1 starts a daily POST to https://app.clawmetry.com/api/license/ping carrying exactly this and nothing else:

{"kind": "selfhosted_ping", "version": "0.12.802", "license": "<sub claim>", "tier": "pro", "ts": "..."}

No hostnames, node IDs, counts, usage, or telemetry. It exists so a license can be checked against revocation and so you are told about updates. Leave it unset and the deployment is silent. Source: clawmetry/selfhosted.py.

Air-gapped

CLAWMETRY_OFFLINE=1 additionally skips the entitlement probe and the pro wheel download, so licensing is fully local against a signed license file. Use it together with SELF_HOSTED=true on a network with no route out.

Local only (default pip install clawmetry)

Runs entirely on 127.0.0.1 against your own filesystem. Nothing is uploaded. Two outbound calls remain: the one-time install ping (DO_NOT_TRACK=1 stops it) and the PyPI version check described below.

Managed cloud (after clawmetry connect)

The daemon pushes to ingest.clawmetry.com on the cadence in the table below. Content is sealed client-side under a key the server never stores and cannot recover. The browser decrypts for display. See ARCHITECTURE.md.

CLAWMETRY_NO_CLOUD=1, or touch ~/.clawmetry/nocloud, turns every upload into a no-op without a restart. It does not gate the entitlement probe (CLAWMETRY_OFFLINE=1 does).


The suppression gate

clawmetry.endpoints.egress_suppressed() is the single check every discretionary outbound call must pass. It returns True when any of:

Condition Meaning
CLAWMETRY_ENDPOINT / CLAWMETRY_INGEST_URL set traffic repointed at your server
endpoint key in ~/.clawmetry/config.json same, via config file
SELF_HOSTED=true / CLAWMETRY_SELF_HOSTED=true this process is your server
CLAWMETRY_OFFLINE=1 air-gapped

"Discretionary" excludes the deployment's own configured ingest endpoint. That push is the product, and repointing it is what self-hosting means.

The gate fails closed: if the check itself raises (partial install, older package), the call is suppressed rather than allowed. Covered by tests/test_egress_suppression.py.


Full destination table

ClawMetry-operated, managed cloud only

Cadence is what the daemon did in the 2026-09-03 capture with Claude Code and Codex sessions on the machine.

Path Cadence Plaintext the server reads Sealed
POST /ingest/heartbeat every 3 s while a viewer is open, else 60 s node_id, platform, version, e2e flag, install_id, detected runtimes with session counts, tool names and counts today, last tool used, detector messages, Ollama model names, billing plan labels, activity counters, store size, cache-key names cache_pushes blobs: last 50 brain events, memory files, cron list, alert rules, approval queue
POST /ingest/sessions every 60 s when rows change session_id, runtime, model, status, timestamps, total_tokens, cost_usd, token_split, cache stats, tool_error_pct, tool_call_count, message_count, surface title_blob (the readable title)
POST /ingest/system-snapshot every ~60 s; about 300 KB once the server advertises gzip (1.2 to 1.4 MB before) node_id 80 keys: transcripts with full message text, machine info, security posture, audit log, agent inventory with workspace paths, MCP servers, tool catalog, spending, traces, skills
POST /ingest/events as transcripts change (OpenClaw JSONL path) node_id raw transcript events
POST /ingest/logs every 60 s when the gateway log grows node_id gateway log records
POST /ingest/stream every 2 s when the log tail moves node_id raw gateway log lines
POST /ingest/memory on file-hash change node_id OpenClaw workspace memory files
POST /ingest/cache only when the cloud asks node_id, cache_key, shape name, args hash on-demand query results and action outcomes
POST /api/ingest (crons) each tick when a job changes job_id, name, cron expression, model, run state none
POST /api/cloud/alerts/dispatch on rule match, checked every 60 s rule id and name, node_id, event id, 500-char summary none
GET /ingest/wake long-poll held 15 s, continuous node_id in the query, key in a header none
GET /api/cloud/policies every ~30 s key in a header none
GET /api/license/entitlement daemon start, then every ~30 min key in a header; an entitled node downloads and installs the pro wheel none
GET /api/cloud/account, GET /api/cloud/claim-status once per daemon start; every 5 s only while on a placeholder account key in a header, node_id none
POST /auth only during clawmetry connect api_key, hostname, machine_id (hash of hardware id) none
POST /api/install once per install, first run random install id, version, OS, Python version, detected agent name, CI flag none
POST /api/desktop/open (stage daemon_failed) at most once per failure class per 6 h, and only when the daemon cannot do its job random install id, failure class from a closed enum (daemon_lock_refused, daemon_ingest_stalled), clawmetry version, OS, OS version, arch, Python major.minor, and a session id derived as <class>-<UTC date> none
POST /api/admin/anon-event first dashboard load that fails auth event name, version, browser family none
POST /ingest/trial-warning at most once a day on a trial days_left, plan none
POST /api/license/ping self-hosted, off unless CLAWMETRY_LICENSE_PING=1 version, license subject, tier, timestamp none

Inbound control channel

The heartbeat response can carry actions for the daemon: pause, stop or kill a session, edit or disable a cron, answer an approval, run an allow-listed DuckDB query and return the sealed result. Every relayed action is written to ~/.clawmetry/audit.db as it arrives, accepted or refused, so the node owner has their own record of what the server asked.

Actions that would run a server-supplied prompt through a local agent (selfevolve_fix, cron_create, cron_fix) are refused unless the node has opted in with CLAWMETRY_ALLOW_REMOTE_PROMPTS=1 or clawmetry config set remote_prompts true.

Third-party

Host Purpose When Default
pypi.org version check; a newer release is pip-installed every 60 s from the daemon (CLAWMETRY_UPDATE_CHECK_SECS). Not contacted by self-hosted, air-gapped or repointed installs on; CLAWMETRY_AUTO_UPDATE=0 stops installs, CLAWMETRY_AUTOUPDATE_MIN_AGE_HOURS adds a stability window. Every attempt is recorded locally
api.anthropic.com / api.openai.com transcript quality scoring with your own key: a redacted 8 KB head-and-tail excerpt per session after a session ends off; CLAWMETRY_EVALS_ENABLED=1 or "evals": true in config
api.anthropic.com alert narration: the alert message and rule id when an alert fires and ANTHROPIC_API_KEY is set. Not contacted by self-hosted, air-gapped or repointed installs on; CLAWMETRY_NARRATOR_ENABLED=0
api.anthropic.com with the Claude Code OAuth token a one-token request to read rate-limit headers every 5 min off; CLAWMETRY_CLAUDE_LIMIT_PROBE=1
api.ipify.org public IP for one cosmetic startup banner line dashboard start, managed cloud only on; any suppression condition

Third-party telemetry inside optional integrations is forced off rather than merely left unconfigured. The DeepEval evaluation bridge, for example, phones PostHog by default upstream; clawmetry/deepeval_bridge.py disables that before importing it.

Not egress, but adjacent: the local write API

The dashboard on localhost:8900 trusts loopback callers without a token. It rejects state-changing requests that carry an Origin header from another site, so a page you happen to have open in another tab cannot drive it. CLAWMETRY_ALLOW_CROSS_ORIGIN_WRITES=1 removes that check for embedders that genuinely need it.

Configured by you, off by default

These fire only if you configure them, to a destination you choose:

Integration Destination
Alert webhooks your URL
PagerDuty sink events.pagerduty.com
Opsgenie sink api.opsgenie.com / api.eu.opsgenie.com
Telegram notifications api.telegram.org
OTLP export your collector

Data ClawMetry does not store

Regardless of mode:

  • The encryption key. Generated locally by clawmetry connect, stored in ~/.clawmetry/config.json and the OS keychain, handed to the browser in a URL fragment (which browsers do not send to servers), and sealed to a paired desk device's own key or typed on the device over your LAN. Cloud blobs are opaque to ClawMetry, and the cloud never mints or stores a key for a node. One path does cross the server: rotating a node's key from the web dashboard sends the new key through the cloud's command queue to reach that node. Rotate with clawmetry connect --enc-key on the node to avoid it.
  • Your source code. ClawMetry reads agent transcripts and metadata, not your repository. Transcripts can quote code your agent read or wrote; those travel sealed.
  • Gateway, bot and provider API keys. Read locally to talk to your gateway and to attribute billing; runtime config files are excluded from every upload.
  • The DuckDB store at ~/.clawmetry/clawmetry.duckdb, unless you enable cloud sync, and then only as ciphertext plus the plaintext metadata listed above.

Repeat the capture

S=$(mktemp -d); mkdir -p $S/home/.clawmetry
# copy api_key, node_id and encryption_key from ~/.clawmetry/config.json into $S/home/.clawmetry/config.json
# symlink the agent directories you want scanned: ln -s ~/.claude $S/home/.claude   (and so on)
python3 - <<'EOF' &
import json, sys, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class H(BaseHTTPRequestHandler):
    def _log(self):
        n = int(self.headers.get("Content-Length") or 0)
        body = self.rfile.read(n) if n else b""
        print(json.dumps({"path": self.path, "headers": dict(self.headers), "body": body.decode("utf-8", "replace")}), flush=True)
        b = b'{"ok": true, "sync_allowed": true, "plan": "pro"}'
        self.send_response(200); self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b)
    do_GET = do_POST = do_PUT = _log
    def log_message(self, *a): pass
ThreadingHTTPServer(("127.0.0.1", 8999), H).serve_forever()
EOF
env -i PATH="$PATH" HOME=$S/home CLAWMETRY_ENDPOINT=http://127.0.0.1:8999 CLAWMETRY_AUTO_UPDATE=0 \
  python3 -m clawmetry.sync

CLAWMETRY_ENDPOINT repoints both hosts, so nothing reaches the real cloud. Every request the daemon makes prints as one JSON line. Decrypt any blob with clawmetry.sync.decrypt_payload(blob, config["encryption_key"]). Delete $S afterwards: it holds a copy of your key.


Reporting a discrepancy

Traffic to a host not listed here is a bug and is treated as a security issue. See SECURITY.md for how to report it privately.