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โ
| Path | What |
|---|---|
~/.whodunit/config.json | Settings: retention, backups, sync target, version check |
~/.whodunit/repos.json | Which repositories you instrumented |
~/.whodunit/data/journal.db | One row per agent edit, scoped by repository |
~/.whodunit/baselines/<repo>.json | Pre-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:
| Table | Contents |
|---|---|
whodunit_repos | Repository id, contributor email, spec version |
whodunit_commits | Contributor email, SHA, timestamp, status, method, agent, purpose, ratio, line and file counts |
whodunit_events | Per edit: contributor email, timestamp, agent, session, tool, file path, lines, hunk hash, outcome, model, branch, MCP server, human-edited |
whodunit_sessions | Per session: contributor email, message and tool counts, tokens, timing, effort, permission mode |
whodunit_event_lines | Line hashes and first-seen timestamps |
whodunit_baselines | Pre-adoption commit counts, median diff size, revert rate, cadence |
whodunit_identities | Two email addresses per row: an alias and the canonical address it resolves to |
whodunit_teams | Contributor email and a team name, published from the teams map in your config |
whodunit_model_prices | List price per model, per million tokens. No user data โ filled from the providers' public pricing pages |
whodunit_schema | Which 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