Logging, diagnostics, and what leaves your machine

August 2, 2026 · View on GitHub

Confidentiality: Public Status: DRAFT - UNREVIEWED

How a Dahrk node logs, how to debug one, and - the part that matters if you are running a node on your own infrastructure - exactly what does and does not leave the machine it runs on.

The short version

A node writes two logs, both local:

FileWhat it is
~/.dahrk/logs/node.out.log, node.err.logThe plain transcript: what the node printed, as it printed it. Captured by the service (launchd / systemd).
~/.dahrk/logs/node.jsonlThe node's structured log: one JSON object per line, with levels, timestamps, correlation ids and full error stacks.

On a node you run, neither is sent anywhere by default. Log shipping is off for self-managed nodes, and the hub refuses a self-managed node's log records even if one is sent. If we need your logs to debug something, we have to ask you for them - and dahrk diagnose exists to make saying yes safe (see below).

The node does report its own health to the hub - uptime, version, active jobs, reconnect count, disk free, error counts. That is metadata about the node, never about your code; see What your node sends below for exactly what it is and how to turn it off.

Reading them

dahrk logs                     # tail the transcript (what a healthy node is doing)
dahrk logs -f                  # ...and keep watching
dahrk logs --level error       # only what went wrong
dahrk logs --run <runId>       # everything about one run, end to end
dahrk logs --run <runId> --json | jq   # the raw records

--level, --run and --json read node.jsonl; on its own, dahrk logs tails the transcript.

--run is the one worth knowing. Every log line the node emits during a stage carries the same identifiers the hub knows the run by (runId, stageId, jobId, attempt). So when a run misbehaves, the hub's view of it and the node's view of it are the same run, and you can put them side by side.

Levels: why the file is more verbose than your terminal

DAHRK_LOG_LEVEL controls what reaches stdout (default info). The file is written at debug regardless.

That asymmetry is deliberate. Debug logging you have to switch on before the incident is close to useless, because you find out you wanted it afterwards. So the node always writes the detailed record to disk, and the terminal stays quiet. Disk cost is bounded: node.jsonl rotates at 10 MB and keeps five generations.

VariableDefaultPurpose
DAHRK_LOG_LEVELinfoLevel for stdout: trace | debug | info | warn | error | fatal | silent.
DAHRK_LOG_FILE_LEVELdebugLevel for node.jsonl.
DAHRK_LOG_FILEonSet 0 / off / false to disable the file sink (containers with ephemeral disks, where stdout is already captured).
DAHRK_CRASH_EXIToffSet 1 to make the node exit on an uncaught exception rather than log it and carry on.
DAHRK_TELEMETRY(unset)off = send the hub nothing about this node. health = health metadata only, never logs. A ceiling the hub cannot raise - see What your node sends.

At debug you additionally see every git operation - clone, mirror refresh, worktree create, fetch - which is usually what you want when a stage fails before the agent ever starts.

Crashes

An uncaught exception or unhandled rejection is logged with a full stack and written to ~/.dahrk/logs/crashes/<timestamp>.json, then the node carries on.

Two deliberate choices there:

  • A separate file, because the log rotates. A crash-loop will happily push its own first cause out of node.jsonl, and the first cause is the one you need.
  • Carry on, rather than exit. In this process the realistic source of a stray rejection is a background best-effort path (shipping a trace, tidying a worktree), and dying costs real work: a node that goes down mid-stage loses the in-memory record it uses to re-send finished results, so the hub's run can stall until it re-dispatches. Logging loudly and surviving is the better trade. Set DAHRK_CRASH_EXIT=1 if you would rather your supervisor restart it.

Recovering a run branch whose commits look lost

Symptom: a run branch (chore/issue-DHK-…, say) appears reset to base and your commits are gone; git branch -a --contains <sha> finds nothing, so the commit looks dangling and one git gc from destruction.

It almost certainly is not. When a new run of the same issue resets a branch that still held unpushed commits, the node parks the old tip before the reset, at a ref outside refs/heads/*:

refs/dahrk/salvage/<branch>/<sha12>

It lives outside refs/heads/* by design - so it claims no branch name and no worktree - which is exactly why git branch --contains never lists it. To find and recover a parked tip:

# in the bare mirror for the repo (~/.dahrk/mirrors/<repo>)
git for-each-ref refs/dahrk/salvage/        # every parked tip, newest sha last
git branch recovered-work <the-ref>         # put it back on a real branch

The node also records how many tips are currently parked on every reap pass, in the EDGE_REAPED log line (dahrk logs), under salvagedRefs.

Lifetime. Parked refs are not kept for ever: the worktree reaper collects one 14 days after it was parked (aged by park time, not the commit's date, so a fresh park always survives its first sweeps). If work has gone missing, recover it well inside that window rather than relying on the ref still being there weeks later.

Secrets

A node handles real credentials, even though it holds none of its own: the hub attaches a git token, MCP keys and inference auth to each job, and DAHRK_GIT_TOKEN carries the git token to a git subprocess for the length of one operation. Everything written to a log passes through a scrubber first (packages/edge/src/redact.ts), which drops:

  • values under any key that looks sensitive (*token*, *secret*, *password*, authorization, ...);
  • credentials embedded in URLs - https://user:secret@host, the shape a git remote takes, and the shape that appears in the git error messages we now log;
  • token-shaped strings anywhere in free text: ghp_…, github_pat_…, glpat-…, sk-…, sk-ant-…, xox…, JWTs, Bearer …, AKIA….

It is deliberately over-eager. A redacted value costs you a re-run; a leaked token costs you rather more.

dahrk diagnose: how to send us a log without regretting it

dahrk diagnose

Writes one local file containing: this node's id, name, tenant, version and host; the doctor verdict; the tail of the structured log; and every crash record. Then it stops.

  • It does not upload. There is no --upload flag, and there is not going to be one.
  • The enrolment token is removed, not redacted - there is no marker left behind to tell anyone it was ever there.
  • Your source code, prompts, and issue content are not in it.

It is plain JSON rather than a zip, for one reason: you can open it and read every byte before you decide to send it. A bundle you have read is one you can consent to. That is the entire design.

What your node sends

Three separate channels, with three different rules. Conflating them is how people end up either paranoid or complacent, and both are wrong.

1. Health telemetry - on, from every node

Your node tells the hub about itself, on each heartbeat: uptime, client version, jobs in flight, how many times it has reconnected, worktrees on disk, free disk, which runtimes it found, and counts of failures by category (git, runtime, policy, hub, internal).

That is the whole list. It contains no file paths, no repository or branch names, no command lines and no error messages. It is numbers, a version string, and category counts - and that is structural, not a promise about our good intentions: the type cannot hold anything else (NodeHealth in @dahrk/contracts). Note the deliberate asymmetry - we ship the count of git failures, never the message, because a count tells us your node cannot clone while a message would tell us which private repository it cannot clone from.

Without this, we cannot tell you that your node is broken. That is why it is on.

2. Log shipping - off on your machines

Your node's log lines carry free text. A failed git operation quotes the remote it could not reach, the branch it was on, the paths it was working with. So:

  • Self-managed node (yours): shipping is off. The hub refuses your log records even if a client sends them - the decision is not the client's to make.
  • Managed node (ours): shipping is on. We operate the machine, and we need its logs to run it.

You can turn shipping on deliberately, and off again. An operator with the portal can also turn it on for a session while debugging a live problem; it reverts when the node reconnects.

3. Agent traces - on, and always have been

While a stage runs, the node streams a trace: the agent's reasoning, its tool calls, and its tool results. Tool results can include file content the agent read from your worktree. That is the product working as designed - it is what renders in the run view and what the harness reasons over - and it is governed by the hub's retention policy. It is a different thing from your logs, and it predates all of the above.

Turning it off

DAHRK_TELEMETRY=off      # no health, no logs. Nothing about the node leaves the machine.
DAHRK_TELEMETRY=health   # health metadata only; refuse log shipping even if the hub asks.

This is a ceiling the hub cannot raise. The hub may ask for less than you allow; it can never ask for more. That is enforced in log-shipper.ts, in a repository you can read - which is rather the point of shipping an open-source client.

(Note that DAHRK_TELEMETRY governs what the node says about itself. It does not disable agent traces, which are how the product works at all; if you do not want a stage's output reaching the hub, do not run that stage.)

Summary

Where it goes
Your source tree and .env filesStay on this machine. Never sent.
Your SSH keys, gh token and model-provider loginsNever read at all. A node authenticates only with what the hub attaches to a job, so your own credentials take no part in a run.
Node logs, crash records, support bundlesStay on this machine on a node you run. Only leave if you enable shipping, or send a bundle.
Node health (uptime, version, counts)Sent to the hub. Metadata only. DAHRK_TELEMETRY=off stops it.
Agent trace events (reasoning, tool calls, tool results - which can quote file content)Sent to the hub while a stage runs.

For the full boundary - every field, both directions - see the harness's docs/data-boundary.md.

Sources

  • packages/edge/src/logger.ts (sinks, levels, rotation), packages/edge/src/redact.ts (scrubbing).
  • apps/edge-node/src/logs.ts (dahrk logs), apps/edge-node/src/diagnose.ts (dahrk diagnose), apps/edge-node/src/process-safety.ts (crash handling).
  • dahrk-harness/docs/data-boundary.md (what crosses the hub boundary).