with one click
askdiff
Start the askdiff WebSocket server for interactive diff review.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Start the askdiff WebSocket server for interactive diff review.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
| name | askdiff |
| description | Start the askdiff WebSocket server for interactive diff review. |
| user-invocable | true |
| allowed-tools | Bash |
Compute the user's diff, write it to a temp file, then launch the
askdiff CLI in the background pointing at that file.
Keep Steps 1–4 in sync with
.claude/skills/askdiff-dev/SKILL.md— only the Step 4cresolve-sessioninvocation and Step 5 launch differ between the two skills.
Look at the message that invoked this skill. Anything after /askdiff is
free-form natural language that may carry two kinds of information:
git diff command.Either or both may be empty. The diff-description part may be empty (working tree); the session hint defaults to "the invoking session" when absent. Treat them independently — first identify and set aside the session hint, then pass the rest to the diff resolution below.
Use only the current /askdiff line's args. Read <command-args>
strictly from the message that invoked this skill — nothing else. Do
not infer or carry over args from earlier conversation turns, from
SessionStart hook context (e.g. a "Previous session summary" block
that quotes a prior /askdiff invocation verbatim), from CLAUDE.md, or
from memory. If the current line has no text after /askdiff, both
diff_description and session_hint are empty/none — that means
working tree + invoking session, full stop.
diff_description | git command | Suggested label |
|---|---|---|
| (empty) | working tree — see Step 2 | Working tree |
last commit | git diff HEAD~1 HEAD | HEAD~1..HEAD |
last 3 commits | git diff HEAD~3 HEAD | HEAD~3..HEAD |
the 5th latest commit | git diff HEAD~5 HEAD~4 | HEAD~5..HEAD~4 |
current branch against feature/test | git diff feature/test...HEAD (three-dot, PR semantics) | feature/test…HEAD |
main vs my branch | git diff main...HEAD | main…HEAD |
abc123 vs def456 | git diff abc123 def456 | abc123..def456 |
staged | git diff --cached | staged |
Defaults when the user is ambiguous:
git diff X...Y) — matches how GitHub renders PRs.git diff A B).git diff HEAD~N HEAD~(N-1)).If the description doesn't fit the table (e.g. "the commit where I added
the favicon", "where we ripped out the old auth"), pin down a single
commit with the ladder below, then diff <sha>^..<sha>. Try in order
until exactly one commit matches; if several match, pick the most recent
and tell the user which one you chose; if none match, stop and ask —
do not guess.
Author. "by ", "'s last", "by my coworker":
git log --author=<pattern> -i -1 --format='%H %an %s'
Commit message. "the migration commit", "where I bumped deps":
git log --grep=<keyword> -i -1 --format='%H %s'
Diff content. "where I added/removed/touched ". -S matches
when a string's count changed in any file; -G is a regex over the
diff text:
git log -S"<distinctive-string>" -1 --format='%H %s'
git log -G"<regex>" -1 --format='%H %s'
File history. When you can identify the file but not the commit (e.g. "where the homepage was added" — search the working tree for a plausible path first, then ask git):
git ls-files | grep -i <hint> # find candidate path
git log --follow -1 --format='%H %s' -- <path> # most recent touch
git log --follow --diff-filter=A -1 --format='%H %s' -- <path> # commit that introduced it
Once a SHA is in hand, build the label as <short-sha>: <one-line gloss>
(e.g. d0b332b: add favicon) and use git diff <sha>^ <sha> as the
diff command. If the user's count and description disagree (e.g. "my 3rd
previous commit, where I added a favicon" but the favicon is at HEAD~2),
trust the description over the count and flag the off-by-one to the
user so they know what you picked.
Stay within git — never read file contents to disambiguate. Use only
git log (with --author, --grep, -S, -G, --follow,
--diff-filter) and git ls-files | grep on path names. Don't cat,
grep -r, rg, or Read working-tree contents. If the ladder doesn't
pin down a unique commit, AskUserQuestion with the candidates — reading
files during search is a token-cost cliff that requires user consent.
Validate every ref first. Run git rev-parse --verify <ref>^{commit} for
each ref the user named directly. If any fails, stop and tell the user
which ref didn't resolve — do not launch the server. (Refs returned by the
search ladder are already validated by virtue of git log finding them.)
By default /askdiff attaches the WS server to the invoking session
(the one running this skill). The user may override that by carrying a
phrase about the target session in their input. Decompose the input into
two parts:
diff_description — what to diff (everything Step 1's table/ladder uses)session_hint — one of none, explicit-id <uuid-or-prefix>, or
keywords <a, b, c, …>A session hint shows up as language about the session/conversation/chat
the diff comes from — e.g. "attached to the session …", "in our session
about X", "from the chat where Y", "session id <uuid>", or a bare
UUID-shaped token (8+ hex chars).
Examples:
| User input | diff_description | session_hint |
|---|---|---|
last commit | last commit | none |
| (empty) | (working tree) | none |
the staleness commit attached to the session where we discussed mtime checks | the staleness commit | keywords: "mtime checks" |
last commit in our session about pricing rules and tax math | last commit | keywords: "pricing rules", "tax math" |
session 322bc90a | (working tree) | explicit-id: 322bc90a |
abc123 vs def456 in session 322bc90a-714f-41b7-914e-109404e46072 | abc123 vs def456 | explicit-id: full UUID |
Be conservative. If parsing is itself ambiguous (e.g. "the foo session"
— is "session" a noun in the diff or a trigger?), treat the whole input
as diff_description (no session hint). Don't ask the user to clarify the
parse — just resolve the diff and proceed; the default attachment to the
invoking session is always safe.
The session hint is consumed in Step 4. Steps 2 and 3 use only
diff_description.
First resolve the parent Claude Code session and project cwd. All /tmp
paths the skill writes (diff file, server log, dev-only UI log/pid file)
key off the session UUID so concurrent /askdiff runs from different
sessions don't collide:
session_file="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/sessions/$PPID.json"
session_id=""
project_cwd="$PWD"
if [ -f "$session_file" ]; then
session_id=$(sed -n 's/.*"sessionId":"\([^"]*\)".*/\1/p' "$session_file")
manifest_cwd=$(sed -n 's/.*"cwd":"\([^"]*\)".*/\1/p' "$session_file")
[ -n "$manifest_cwd" ] && project_cwd="$manifest_cwd"
fi
suffix="${session_id:-pid-$$}"
diff_file="/tmp/askdiff-diff.$suffix"
No random component on the diff file — re-invoking /askdiff from the
same session overwrites in place, which is exactly what a refresh would
do. Different sessions get different suffixes and don't collide. (If
launched outside a CC session, session_id is empty and the suffix
falls back to pid-<bash-pid> so we still avoid collisions.)
Working tree (no description). Untracked files don't appear in
git diff HEAD, so we union them in via --no-index:
{
git -C "$project_cwd" diff HEAD --no-color
git -C "$project_cwd" ls-files --others --exclude-standard -z \
| while IFS= read -r -d '' f; do
git -C "$project_cwd" diff --no-index --no-color -- /dev/null "$f" || true
done
} > "$diff_file"
(In an empty repo with no HEAD, replace HEAD with the empty-tree SHA
4b825dc642cb6eb9a060e54bf8d69288fbee4904.)
Description path. Just run the resolved command:
git -C "$project_cwd" diff <args> --no-color > "$diff_file"
For the description path, if the resulting file is empty, stop — tell the user the requested diff is empty and don't launch. The working-tree path can legitimately be empty (clean tree); launch anyway and the UI will show "No changes."
Set volatile=1 for the working-tree path, volatile=0 otherwise — Step 5
forwards this as ASKDIFF_DIFF_VOLATILE (gates per-file mtime staleness).
Use the "Suggested label" column above. For the working-tree case, use
Working tree. Keep it under ~40 chars. This becomes ASKDIFF_DIFF_LABEL.
Compute attached_session and session_source from session_hint
(from Step 1). Default is the invoking session.
attached_session="$session_id" # default = invoking
session_source="invoking"
sessions_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects/$(echo "$project_cwd" | tr '/' '-')"
If session_hint is none, leave the defaults and skip to Step 5.
If session_hint is explicit-id <X>:
explicit_id="<X>"
if echo "$explicit_id" | grep -qE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then
# Full UUID: trust it if the file exists.
if [ -f "$sessions_dir/$explicit_id.jsonl" ]; then
attached_session="$explicit_id"
session_source="explicit"
else
# → AskUserQuestion: session not found, use current?
:
fi
else
# Short prefix: list and disambiguate via `find` (zsh-compatible — see
# footgun in CLAUDE.md: `shopt` is bash-only and silently fails in zsh,
# and zsh arrays are 1-indexed so `${matches[0]}` returns empty).
matches=()
while IFS= read -r f; do
[ -n "$f" ] && matches+=("$f")
done < <(find "$sessions_dir" -maxdepth 1 -name "${explicit_id}*.jsonl" -type f 2>/dev/null)
case ${#matches[@]} in
1)
# Iterate to dodge bash-vs-zsh first-index difference.
for f in "${matches[@]}"; do
attached_session=$(basename "$f" .jsonl)
session_source="explicit"
done
;;
0)
# → AskUserQuestion: no session matches "<prefix>", use current?
: ;;
*)
# → AskUserQuestion: pick one of N candidates (list short-uuid · age)
: ;;
esac
fi
AskUserQuestion branches: 0 matches → "Use current session" or "Cancel"
(don't launch). Multiple matches → one option per UUID as
<short-uuid> · <age> plus "Use current session"; on user pick set
attached_session and session_source="explicit".
If session_hint is keywords <a, b, c, …>, call the CLI's
resolve-session subcommand. It searches recent project JSONLs (mtime
−30d, excluding the invoking session, top 5 by hit count) and prints
single-line JSON: {"candidates":[{"uuid":"…","count":N,"age":"…"}, …]}.
# Pinned by the build script for the npm tarball; in-repo stays "latest".
ASKDIFF_VERSION="latest"
results=$(
npx -y askdiff@"$ASKDIFF_VERSION" resolve-session \
--cwd "$project_cwd" \
--invoking "$session_id" \
--diff-file "$diff_file" \
--keyword "<keyword 1>" \
--keyword "<keyword 2>" \
--sha "<sha1>" \
--sha "<sha2>" \
--branch "<branch1>" \
--branch "<branch2>"
)
echo "$results"
Repeat --keyword/--sha/--branch per value; omit a flag entirely
if its list is empty. Always pass --diff-file (changed file paths feed
the search as additional needles regardless of diff source); omit --sha
and --branch for working-tree diffs (no commit/branch context).
Read $results and route on .candidates:
| Result | Action |
|---|---|
| empty | AskUserQuestion: "no session matched <keywords>. Use current?" → "Use current" or "Cancel and refine" |
| 1 candidate | use that UUID; attached_session=$uuid, session_source="matched" |
| 2+, top count ≥ 2× second | use top-1; session_source="matched" |
| 2–5, comparable counts | AskUserQuestion: one option per candidate as <short-uuid> · <age> · <count> hits, plus "Use current session" |
Don't widen scope automatically (e.g. by raising --max-age-days or
--top). Surface 0/unclear results via AskUserQuestion; re-run only on
user request.
Run as a single Bash command. Substitute EXTRA_DIFF_FILE and
EXTRA_DIFF_LABEL literally with the values from Step 2/3.
set +e
# Pinned by the build script for the npm tarball; in-repo stays "latest".
ASKDIFF_VERSION="latest"
# Filled in by Steps 2/3 — keep Step 2's preamble (session_id, project_cwd,
# suffix) and Step 4's resolution (attached_session, session_source) above.
EXTRA_DIFF_FILE=""
EXTRA_DIFF_LABEL=""
log_file="/tmp/askdiff.$suffix.log"
pid_file="/tmp/askdiff.$suffix.pid"
# 1. Kill any previous server for this session and reuse its port — keeps
# the open browser tab valid (the WS auto-reconnects).
saved_port=""
if [ -f "$pid_file" ]; then
read -r old_pid saved_port < "$pid_file" 2>/dev/null
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
kill "$old_pid" 2>/dev/null
if [ -n "$saved_port" ]; then
for _ in $(seq 1 20); do
lsof -iTCP:"$saved_port" -sTCP:LISTEN -t >/dev/null 2>&1 || break
sleep 0.1
done
fi
fi
rm -f "$pid_file"
fi
# 2. Launch. Reuse port if we have one; otherwise the CLI picks 7837+.
port_arg=""
[ -n "$saved_port" ] && port_arg="--port $saved_port"
cd "$project_cwd" \
&& ASKDIFF_SESSION_ID="$attached_session" \
ASKDIFF_PROJECT_CWD="$project_cwd" \
ASKDIFF_DIFF_FILE="$EXTRA_DIFF_FILE" \
ASKDIFF_DIFF_LABEL="$EXTRA_DIFF_LABEL" \
ASKDIFF_DIFF_VOLATILE="${volatile:-0}" \
nohup npx -y askdiff@"$ASKDIFF_VERSION" --no-open $port_arg > "$log_file" 2>&1 &
new_pid=$!
disown
# Wait for the listening line. (`command grep` — see Step 4c footguns.)
for _ in $(seq 1 60); do
command grep -q "listening on" "$log_file" 2>/dev/null && break
sleep 0.25
done
# 3. Persist <pid> <port> for the next /askdiff invocation to find and replace.
port=$(sed -nE 's|.*listening on http://localhost:([0-9]+).*|\1|p' "$log_file" | head -1)
[ -z "$port" ] && port=7837
echo "$new_pid $port" > "$pid_file"
url="http://localhost:$port/"
# Auto-open only on first launch — refresh re-invocations have a tab open.
if [ -z "$saved_port" ]; then
(open "$url" >/dev/null 2>&1 || xdg-open "$url" >/dev/null 2>&1) &
fi
head -10 "$log_file"
echo ""
[ -n "$saved_port" ] && echo "Refreshed: same port, new diff. Browser tab will auto-reconnect."
echo "UI: $url"
echo "Log: $log_file"
echo "PID: $new_pid (saved to $pid_file)"
# 4. Update check (after launch, never blocking; skipped at "latest" or
# when ASKDIFF_SKIP_UPDATE_CHECK is set; network failures silently ignored).
if [ -z "$ASKDIFF_SKIP_UPDATE_CHECK" ] && [ "$ASKDIFF_VERSION" != "latest" ]; then
latest=$(curl -fsSL --max-time 2 https://registry.npmjs.org/askdiff/latest 2>/dev/null \
| sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -1)
if [ -n "$latest" ] && [ "$latest" != "$ASKDIFF_VERSION" ]; then
echo ""
echo "── A new version of askdiff is available ──"
echo " installed: $ASKDIFF_VERSION"
echo " latest: $latest"
echo " to update: npx -y askdiff@latest install-skill --force"
echo " (add --global if you installed user-level)"
fi
fi
The user already sees UI: / Log: / PID: / Refreshed: / the
new-version block in the bash output. After launch, only narrate:
$session_source is explicit or matched, say so (e.g.
"attached to matched session 322bc90a (was: invoking)") so the user
knows asks aren't landing in the current session's transcriptAskUserQuestion needed