Skip to main content

v0.6.1

September 9, 2026

Added​

  • Files an agent writes through a shell command are now attributed. An agent editing through Edit/Write was recorded with a file; the same edit made with cat > main.go <<'EOF' was recorded as a bare tool call with no file, so it was invisible to attribution and the commit resolved unmatched — or unassisted, a positive claim that no AI was involved. On one machine's journal that was 7,263 Bash records against 894 Edit/Write ones. The command is now read for the files it writes and only for those: a write operator has to name a target, so ls -l x.go and mkdir -p build still record nothing, while cat > f <<'EOF' … && git add -A records f. The heredoc body never reaches the journal — a path is a name, the bytes after <<'EOF' are file content — and that is asserted by test through the real parse path. No line counts and no hunk hash are recorded, because the command carries no diff and a fabricated one would let a shell edit claim intersected on evidence that does not exist; a command that also writes somewhere it cannot name is marked partial so an incomplete file set never reads as a complete one. Measured with the shipped code across 31,873 real Bash calls in six repositories: 91.1% of writing commands name a target, ranging 84.5% to 100% by repository — the spread is the point, since this depends on a project's habits. Cost on a 205 MB transcript is 2.646s against a 2.603s baseline, under 2%. Two known limits: the rule set is per-ecosystem (gofmt -w is 8.5% of writes in a Go repo and 0% in a Python one), and PowerShell's Out-File/Set-Content are not recognised (WHO-238).

Fixed​

  • Agent transcripts were invisible in any repository whose path contained a dot, an underscore, or a space, and their commits were stamped as written by a human. Claude Code names its transcript directory by replacing every non-alphanumeric character in the working directory with a dash; whodunit replaced only the path separators and dropped the colon, so ~/.orion/projects/x/repo was sought at -Users-me-.orion-… while the client had written -Users-me--orion-…. The two names never met. Measured on one machine: 91 git repositories, 14 found under the old encoding and 72 under the corrected one, with 156 transcript directories sitting unread. The colon is now mapped rather than dropped, matching the client — keeping C:\repo distinct from C-\repo was deliberate, but a slug that is unique and wrong finds nothing at all. dun repos candidates decodes the wider encoding by searching which reading exists on disk. A slug over 200 characters is now truncated and suffixed with a base36 hash of the full path, exactly as the client does, so deep worktree paths are found too — the hash was identified by experiment rather than assumption: a directory was created with a 241-character slug, Claude Code was run in it, and the name it wrote was matched (WHO-237).

  • unassisted is no longer stamped when the tooling never looked anywhere real. unassisted asserts that no AI was involved, and that claim was reached whenever no transcript was found — including when the directory searched did not exist, because an absent directory and a present-but-empty one both yield no files and no error. With the encoding bug above, commits an agent wrote end to end were recorded as human work. A commit now falls back to undetermined — "could not tell", not "nobody did" — when no agent's per-repository transcript directory exists and none produced a session, and the hook log says so and points at dun verify. A directory that exists and is empty is still real evidence and still yields unassisted (WHO-236, NAV-21). This also changes behaviour for agents that keep one global transcript store rather than a per-repository one — Antigravity and Codex — where a repository the agent was never run in now reports undetermined instead of unassisted: the store existing says the agent is installed, not that this repository was examined.

  • Note on commits already stamped. Upgrading fixes what is written from now on; it does not rewrite trailers already in your history. A commit stamped unassisted by an affected version stays that way, and the honest reading of one is "this may not have been measured" rather than "no agent was involved". The journal is unaffected — it recorded the agent activity all along — so dun sync republishes the corrected picture to a datalake without touching git history.

  • Windows: a repository under an 8.3 short name found no transcripts. GitHub's own runners work under C:\Users\RUNNER~1, and the tilde was not among the characters the slug decoder would restore, so every decode on such a path returned nothing. Found by CI rather than by review — the failure is invisible on a machine whose paths happen to carry no tilde, exactly as the encoding bug above was invisible on a checkout whose path happened to carry no dot.

  • dun repos candidates no longer scans exhaustively. The wider path encoding made decoding ambiguous — a dash in a directory name is indistinguishable from a path separator — and the first fix searched every reading without bounding the work, taking the command from 4.8s to 35s on a machine with 187 transcript directories, almost all of it os.Stat syscalls. A segment is now verified the moment it closes, at most four dashes are read as literal characters, and stat results are memoised: 2.2s, faster than before any of this work. A slug over 200 characters is not reversible, so such a path is not listed — a limitation of discovery, not of attribution.

  • The truncation hash folded bytes where the client folds UTF-16 code units, so it diverged for any non-ASCII path: /Users/nav/café/repo hashes to vhp8hq in the client and 1d1uy2d under byte folding, and the derived directory name then exists nowhere. Exact for the Basic Multilingual Plane, which is every realistic path; a character outside it is a surrogate pair in UTF-16 and one rune here, so it would still diverge — left alone deliberately, since the failure stays a missed transcript rather than a wrong attribution.

  • Homebrew and Scoop were not updated by the automatic release. release.yml runs as a workflow called from tag-on-prd.yml, and a called workflow sees none of the caller's secrets unless the caller passes them, so publish-packages found PACKAGING_TOKEN unset, warned, and reported success. v0.5.0 and v0.6.0 both shipped with the tap and the bucket still at 0.4.0. The caller now passes secrets: inherit; 0.6.0 was published to both by hand with the repo's own scripts.

Known limitations​

  • Whether the truncation hash renders signed or unsigned is unresolved. JavaScript's bitwise operations yield a signed value and toString(36) renders a negative one with a leading -, which would make roughly half of all slugs over 200 characters differ from what this produces. Both directories observed from the client hash positive, so that branch has never been exercised, and the one pre-existing long directory cannot settle it either — its truncated prefix already ends in -. It degrades safely: a missed transcript, never a wrong attribution. Settling it needs one more observed directory whose path is known to hash negative (WHO-237).
  • Bash write-detection is per-ecosystem. gofmt -w is 8.5% of writes in a Go repository and 0% in a Python one, where the equivalents are black and ruff format. An unrecognised mechanism costs attribution, never correctness — the command yields no path rather than a wrong one.
  • PowerShell's Out-File, Set-Content and Add-Content are not recognised. Redirects, python -c and git-bash heredocs already work on Windows, so a Windows user is not at zero. Which shell Claude Code spawns there has not been measured, and guessing is how the 8.3 bug above shipped.

Full changelog: https://github.com/navjyotnishant/whodunit/compare/v0.6.0...v0.6.1


Download v0.6.1 · Upgrading