Skip to main content
Version: Next

Privacy

Collection makes no network callsโ€‹

The hooks, the daemon and dun ingest read git and local transcripts and write a local SQLite journal. Nothing contacts the network.

The one exception is opt-out and unrelated to collection: dun checks once a day whether a newer release exists, on bare dun only, never from a hook. Disable it with dun config set version_check off, or DUN_NO_VERSION_CHECK / DO_NOT_TRACK in the environment.

What is never recordedโ€‹

The journal schema has no field that could hold prompt text, message content, file contents, hostnames or remote URLs. This is a structural guarantee rather than a filtering step โ€” there is nowhere to put them.

Explicitly out of scope, and named because they were considered and rejected: Claude Code's toolUseResult as a whole (it carries stdout, stderr, and the before/after text of every edit), Codex's agent_message.message, agent_reasoning.text, user_instructions and unified_diff, and Antigravity's CodeContent and ReplacementContent.

Two more are countable but never stored, because they carry user intent verbatim: MCP tool-call arguments, and web-search queries.

A Bash command line is read and not kept. This is the one place whodunit looks at text that can contain file contents, so it is worth being exact about. An agent that writes a file with cat > notes.md <<'EOF' โ€ฆ EOF puts the file's whole body inside the command. Since 0.6.1 that command is parsed for one thing only: which file was written, so that shell edits are attributed like any other.

Only the path comes out. The body, and the command itself, go out of scope with the line that was read: neither is written to the journal, and there is no column that could hold either. A test asserts this end to end by putting a sentinel string in a heredoc and failing if it appears anywhere in the resulting entries.

What is recorded, and identifies someoneโ€‹

Stated plainly, because a privacy page that omits this is worse than none โ€” a reader who finds it later has reason to distrust everything else on the page.

File paths. The journal stores which files an agent edited. That reveals what someone worked on, not merely how much.

A contributor email. whodunit_repos stores the git committer email for the repository โ€” the same address already in every commit you author. A shared database needs it to attribute anything to anyone. The same address is carried on commit and event rows, so a per-person view resolves without a join.

Both stay local until you run dun sync.

Team membership is not a third thingโ€‹

The optional teams block in your config maps those addresses onto team names, so a dashboard can report a group rather than a list of named individuals.

It adds no new observation of anyone. The addresses are already here; which team someone is on is a fact about the organisation chart, and it is one you wrote down yourself rather than one dun inferred from anything it watched. Nothing reads it from your machine, and nothing derives it from your activity.

If you configure no teams, everyone resolves to (unassigned) and the dashboards work unchanged. Someone in no team is never dropped from a filter โ€” a person disappearing from a dropdown is the one failure this whole area is designed against.

Where it livesโ€‹

PathWhat
~/.whodunit/config.jsonSettings: retention, backups, sync target, version check
~/.whodunit/repos.jsonWhich repositories you instrumented
~/.whodunit/data/journal.dbOne row per agent edit, scoped by repository
~/.whodunit/baselines/<repo>.jsonPre-adoption snapshots

Everything is owner-only โ€” 0700 directories, 0600 files โ€” and repaired on open if it was ever created more permissively. On Windows, where os.Chmod cannot express that, the files carry an access control list naming only the owner.

Nothing is ever written into your repositories, committed or pushed.

dun journal show # exactly what has been recorded, in full
dun journal purge # delete it, for this repository only

What dun sync sendsโ€‹

dun sync is the one command that transmits anything. It is never run for you and does nothing until you configure a target.

Ten tables, listed in full so there is no need to take this on trust:

TableContents
whodunit_reposRepository id, contributor email, spec version
whodunit_commitsContributor email, SHA, timestamp, status, method, agent, purpose, ratio, line and file counts
whodunit_eventsPer edit: contributor email, timestamp, agent, session, tool, file path, lines, hunk hash, outcome, model, branch, MCP server, human-edited
whodunit_sessionsPer session: contributor email, message and tool counts, tokens, timing, effort, permission mode
whodunit_event_linesLine hashes and first-seen timestamps
whodunit_baselinesPre-adoption commit counts, median diff size, revert rate, cadence
whodunit_identitiesTwo email addresses per row: an alias and the canonical address it resolves to
whodunit_teamsContributor email and a team name, published from the teams map in your config
whodunit_model_pricesList price per model, per million tokens. No user data โ€” filled from the providers' public pricing pages
whodunit_schemaWhich schema version this database has had applied. One row, no user data

Six of the ten carry a contributor email address: whodunit_commits, whodunit_events, whodunit_sessions and whodunit_repos key their rows on it, whodunit_teams maps it to a team name, and whodunit_identities exists precisely to say that two addresses are the same person, so it holds both. That last one is worth understanding before you publish it: it is what makes a per-person view span someone's machines and aliases. The remaining four โ€” whodunit_event_lines, whodunit_baselines, whodunit_model_prices and whodunit_schema โ€” carry no personal data at all.

The repository id is its root commit SHA โ€” stable across clones and machines, and identifying the repository without revealing its name or remote. A filesystem path would break when the repository moved; a remote URL would record the org and repository name, which this tool never stores.

Not sent: prompt text, message content, file contents, and the lines themselves.

dun sync --dry-run # print the exact payload before any target is contacted