v0.6.1 // hold my coffee, I'll explain this PR

Your PRs, told as a story — not a wall of diffs.

dad reads the whole pull request, groups changes by behavior into chapters, and walks you through them with prose, risk levels, and inline comments. Think of it as a code review with a thicker mustache.

See an example →
Chapter 1 · low risk
Once upon a refactor, CartService learned to forget — quietly, on checkout.
Chapter 3 · ⚠ medium
But the cleanup task still fires hourly. We've got duplicate work, kiddo.
// the example

Prose and hunks, sitting in a tree.

Each chapter weaves together the prose explanation with the actual diff hunks it's talking about — interleaved, not separated. Read the story, see the code in the same breath, and never lose the plot.

Review with care
Widens webhook payload acceptance to raw bytes (string, Uint8Array, ArrayBuffer), computing HMAC over the exact wire bytes instead of a JSON.stringify round-trip. Preserves the legacy parsed-object path for back-compat.
1

toSignableString — the HMAC now operates on raw bytes

high risk Mark reviewed

Before: computeSignature always called JSON.stringify(payload) before computing the HMAC — even when the caller had the exact bytes the server originally signed. Any semantically-equivalent-but-bytewise-different JSON (whitespace, key order, unicode escapes) would produce a valid signature, opening a window for byte-level mutation.

Now: toSignableString dispatches on the runtime type: strings and binary buffers pass through as-is (decoded to UTF-8 for buffers), and only plain objects fall back to JSON.stringify. The HMAC is computed over whatever toSignableString returns.

This is the highest-risk hunk in the PR: it changes what bytes are fed into the cryptographic signature. Dad would like you to give this one a real look.

Terse Normal Verbose ↻ Re-narrate + Ask AI 💬 Comment on chapter
📄 src/common/crypto/signature-provider.ts L63–L74
64 async computeSignature(
65 timestamp: any,
66 payload: any,
75+ payload:
76+ | string
77+ | Uint8Array
78+ | ArrayBuffer
79+ | Record<string, unknown>
80+ | unknown,
67 secret: string,
68 ): Promise<string> {
69 payload = JSON.stringify(payload);
70 const signedPayload = `"${timestamp}.${payload}"`;
83+ const signable = toSignableString(payload);
84+ const signedPayload = `"${timestamp}.${signable}"`;
72 return await this.cryptoProvider.computeHMACSignatureAsync(
73 signedPayload,

The new toSignableString helper is a pure function with straightforward branching. Note the fallthrough: anything that isn't a string, Uint8Array, or ArrayBuffer gets JSON.stringify'd — including null, undefined, numbers, or any other unexpected type. This is the legacy back-compat path. Heads up: callers who accidentally pass a non-buffer, non-string value get silent behavior rather than an error.

📄 src/common/crypto/signature-provider.ts L92–L99
92+// Raw bytes path (string / Uint8Array / ArrayBuffer) reproduces the exact
93+// bytes WorkOS signed on emit. Object path is legacy back-compat — vulnerable
94+// to any on-the-wire mutation that round-trips through JSON.parse →
95+// JSON.stringify to the same canonical form (whitespace, key order, escapes).
96+function toSignableString(
97+ payload:
98+ | string
99+ | Uint8Array | ArrayBuffer
0 of 4 chapters reviewed · 0 pending drafts
// install

One binary. No assembly required.

Homebrew gets you a standalone binary — nothing else needed. Building from source needs Bun, which is fast, like a dad on the way to Costco at 7am.

recommended · "the easy way"

Homebrew

$ brew install nicknisi/diffdad/dad
Standalone binary. No node, no bun, no nonsense.
for hackers · "the dad way"

From source

$ git clone https://github.com/nicknisi/diffdad \
    && cd diffdad
$ bun install && bun run build
$ bun link
Requires Bun. Hack on the CLI or web UI to your heart's content.
// usage

Five commands, three flags, one mustache.

The CLI fetches a PR, generates the narrative, starts a local server, and opens your browser. That's it. Dad's been doing this for years.

// commands
dad <pr>Open a PR as a narrated story
dad review <pr>Same as above (explicit)
dad configConfigure AI provider, GH token, display
dad cache clearWipe cached narratives
dad --versionPrint version

// PR formats
https://github.com/o/r/pull/123Full URL
owner/repo#123Shorthand
139Bare # — infers from git remote

Flags (any position)

--with=claude|piForce a specific AI CLI
--no-cacheRegenerate narrative even if cached
--no-openDon't auto-open the browser
--port=3000Use a specific port

Try it

// features

Built for semantic review.

Everything is local — narratives are cached, the server runs on your machine, and the frontend never talks to GitHub directly. (Dad doesn't share passwords. Don't ask.)

§

Semantic chapters

AI groups hunks across files by behavior — not filename. Each chapter has a title, risk level, and prose explaining what changed and why.

Two-way GitHub sync

Comments flow both directions — replies you post in dad land in GitHub as real review comments, and GitHub comments show up next to the matching line. No tab-juggling required.

Auto re-narrates on new commits

Push a fixup? Dad notices and regenerates the story for the new SHA. Old narratives stay cached so you can flip between revisions and see how the plot thickened.

💬

Inline comments

GitHub review comments render next to the relevant lines. Bot comments (Greptile, CodeRabbit) cluster into collapsible groups so they don't bury the humans.

📡

Live SSE updates

An SSE connection polls every 10 seconds. New comments, CI status, and check runs appear in real time. Like a baby monitor, but for your PR.

Story controls

Toggle terse / normal / verbose narration per chapter. Re-narrate through different lenses (security, performance, API consumer). Choose your own adventure.

?

Ask AI

Ask follow-up questions about a specific chapter's code. Dad keeps the chapter context so answers stay grounded — no off-topic ramblings about the lawn.

Submit reviews

Comment, Approve, or Request Changes — directly from the UI. Inline comments post to GitHub with your summary. Then dad pats you on the back.

Keyboard-first

j/k to move chapters, r to mark reviewed, c to comment, ? for help. Stays out of your way like a good dad on Saturday morning.

// configuration

Bring your own brain. And token.

Dad uses your existing Claude subscription by default — no API key needed. Swap providers any time with dad config. It's like swapping out the grill propane. Easy.

AI provider

// priority: --with > dad config > auto-detect
  • Claude CLIDefault. Uses your Claude subscription via claude -p.
  • pi CLIFallback if Claude Code isn't installed.
  • Anthropic APIRequires ANTHROPIC_API_KEY.
  • OpenAIRequires an OpenAI API key.
  • OllamaLocal models. No key needed.

GitHub token

// checks in order
  • 1.DIFFDAD_GITHUB_TOKEN environment variable
  • 2.gh auth token from the GitHub CLI
  • 3.Token saved via dad config

Caching

// ~/.cache/diffdad/{owner}-{repo}-{number}-{sha}

Same commit = instant reload. New commit = fresh narrative, automatically. Use --no-cache to force a re-tell, or dad cache clear to wipe it all.

// keyboard

Drive it from the home row.

j ork
Next / previous chapter
r
Toggle reviewed on current chapter
c
Open comment composer on hovered line
?
Show shortcuts help
Esc
Close open panels

Try it on your next PR.

One brew install, your existing Claude subscription, and a fresh pot of coffee. Dad'll take it from here.

View on GitHub →