session-end — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited session-end (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Close out a session with an evidence-grounded record — and, when work is still in flight, hand off so a fresh session can pick up with zero loss. Two intents, one flow: ending always produces the record; handing off (the mid-flight mode) adds the exact resumable state + a copy-pasteable continuation prompt for the next session (you, with an empty context window) and for the human operator. Optimize for truth and resumability, not for sounding complete.
A full run can take a dozen or so turns [estimate], and each one re-reads a whole end-of-session context, so spend it where the record gets used. Handoffs are read by the next session that continues the work, by a project's status and wiki passes, and by the weekly work log. None of those need a handoff for work that already lives in a pull request: git and the PR body carry it.
decisions, diagnoses, reversals or findings that are not already written in a PR body, an issue, a board row or a committed doc. Design, research, debugging and review sessions usually qualify.
in flight. Run the Step 1 probe once, honour the close-out contract if the project declares one (Step 4b; releasing a claim is never optional), ask the Step 4c question, and close in chat with the PR and issue links plus the stop marker. Write no handoff file. Say "light close" and the reason in the closing message, so the choice is visible and the operator can ask for the full record.
Turn budget. Evidence in one call (the Step 1 probe), the handoff in one write, then the contract and the Step 4c question, then at most one amendment covering both (a contract that changed state, a wiki PR that landed or failed), then the closing message. Do not run the probes one at a time, and do not re-read files that are already in context.
A summary written from recollection will hallucinate. Reconstruct the session from hard evidence first, then narrate. This is what makes the handoff safe.
The next session reads this doc cold and acts on it. An untagged claim is indistinguishable from a measurement — a diagnosis you reasoned your way to looks exactly like a fact you checked. That is the failure mode this rule exists to stop, and it is not hypothetical:
A handoff stated: "The `review-pr` skill template is the source of that divergence."
>
It reads as a diagnosis. It was an inference, and it was wrong — the divergence was a deliberate safety separation between a code review that runs the tests and one that doesn't. The next session nearly "repaired" it, which would have let an explicitly-untested review auto-merge its own changes. It was caught only because that session happened to open both files. The same handoff also stated a mechanism wrongly (a script reading the cwd's .env, when it actually resolves from the script's own location) — harmless there, same root cause.The rule. Every load-bearing claim carries an epistemic tag, or states its evidence inline so the reader can judge for themselves. Load-bearing means: the next session is likely to act on it without re-deriving it — a diagnosis, a mechanism, a state-of-the-world assertion, a number, a "X is why Y". Passing colour and narration need no tag; don't tag everything, or the tags stop carrying signal.
| Tag | Means | Requires |
|---|---|---|
[verified] | You read it, ran it, or observed it. | Name how — the file, command, or output. |
[derived] | You inferred it from evidence. Sound reasoning, still an inference. | Name what it's inferred from, so the reader can re-run the inference. |
[assumed] | No source; you took it as given. | Say what would confirm or falsify it. |
[unverified] | Asserted during the session, never checked. | Say what checking it would take. |
[derived] is the tier that was missing, and it is the one that bites: an inference is grounded enough to feel verified, so it gets written in the voice of a measurement. If you reasoned your way to it, it is `[derived]`, however confident you are.
Stating the evidence inline satisfies this too, and is often shorter and better: "`review-pr/SKILL.md` has no test step and `code-review` does" beats a bare tag — it hands the reader the same evidence you had.
The gate: no claim without a source. If you can't name what a load-bearing claim rests on, it is [assumed] — write it as [assumed], or leave it out. Never close the gap by writing more confidently. This applies to every section below, and hardest to Steps 3 and 5, where claims travel furthest from the evidence that produced them.
Run the probe, once:
sh "{{SKILL_HOME}}/probe.sh" "<session start, e.g. 2026-09-16 09:00>"That one call returns every git and GitHub check listed below: the primary checkout, branch, status, diff stat, the window's commits on this branch and on the default branch with the files they touched, this author's merged and recently updated open pull requests, the handoff path to write (timestamp and random token already generated), the origin stamp line, whether the handoffs folder is ignored, and the project's close-out contract if it declares one. It is read-only and does not fetch. If the script is missing, run the same checks as ONE compound command. The bullets below stay because they say what each check is for and how to read its output; they are not a list of separate calls.
git status --short and git diff --stat (+ git log --oneline -15) — what actually changed / was created. Every artifact you cite must appear here or on disk.two probes are structurally blind to anything that has merged: a session whose work went out through PRs has a clean tree by construction, and reporting "no artifacts" there records a productive shift as an empty one. A dirty tree does not clear you either. Merge a PR and leave one unrelated edit or stray untracked file, and the status probe is non-empty, so a check that fires only on "both empty" never runs and the handoff silently keeps the stray file while dropping the merged work. The session window is what decides this, not tree cleanliness, so run these every time:
git log --oneline --since=<session start> (and on the default branch) — what landed during the shift.git show --stat <sha> per commit, or git diff --stat <base-branch>...HEAD — the files those commitstouched. (Three-dot already means "since the merge base"; don't pass a merge-base into it.)
gh pr list --state merged --author @me --search "merged:>=<session start date>" --limit 100 — PRsthat closed during the shift. Bound it by both date and author, and set --limit explicitly: an unfiltered window sweeps in every other contributor's merges, and --limit defaults to 30 and truncates silently, which is the same under-reporting this whole bullet exists to stop. If you get back exactly the limit, assume you hit the ceiling and raise it rather than reporting a round number. Filtering is necessary but not sufficient — --author @me still collapses concurrent agent sessions running under one account, so correlate each candidate with this session before claiming it (does its branch, timing, or content actually match work you did?). Landing in the window is not evidence you did it, and an artifact list is exactly where a borrowed claim does damage.
artifact (a published page, a scheduled task, a review verdict left on a PR). If this turns up nothing either, say so as an explicit finding — "no files changed; this shift's output was X" — never as a silently omitted section.
Include only the sections that apply to this session (a design session, a debugging session, and a research session produce different shapes). Always tag epistemics — this mirrors a verify-first standard.
[verified] / [derived] / [unverified] / [assumed]. Not just numbers — a diagnosis ("X is the source of Y"), a mechanism ("this script reads Z"), and a state-of-the-world assertion are claims too, and are the ones that mislead. Never present an unverified figure or an inference as fact. Note how the verified ones were checked and from what the derived ones were inferred.git status —or, when the tree is clean because the work merged, from the widened evidence of Step 1 (the landed commits and the files they touched). Say which, so "merged" never reads as "nothing happened".
Keep it scannable (ranked, terse). Match length to session size; don't pad.
If a task was underway when handoff was invoked, capture the exact resumable state:
Everything here is load-bearing by construction — it exists precisely so the next session acts on it without re-deriving it. So tag it ([verified] / [derived] / [unverified] / [assumed]) or state the evidence inline. Be strictest about any claim that would justify changing something: "this looks wrong / is the cause / should be repaired" is a [derived] claim until you have read the thing and can say why it is that way. If it might be deliberate, say so — that sentence is what stops the next session from "fixing" a safety property.
Write the full summary to a file so the next session can READ it rather than trust pasted prose: <primary-checkout>/.claude/handoffs/<YYYYMMDDTHHMMSSZ>_<slug>_<discriminator>.md (create the dir; it's additive/safe). If not in a writable repo, skip and emit in-chat only. The Step 1 probe prints this path with the timestamp and token filled in, plus the origin stamp line; use them as printed and supply only the slug. A second write in the same session gets a new token (rerun the probe or generate one).
Every filename carries a unique discriminator from the start — not only when a clash is noticed. That directory is shared by every worktree (see below), so two concurrent sessions can land on one path, and losing a handoff to a silent overwrite is no better than losing it to worktree cleanup. Checking whether the file exists and only then adding a suffix does not fix this: both sessions can look, both can see nothing, and both can write the same path. Make the name unique before you write it.
The discriminator is a fresh random token per write (6-8 chars is plenty), generated at the moment you write, not derived from anything. Don't reach for the session id: it is constant within a session, so invoking session-end twice in the same second with the same slug would rebuild the same path, and two different sessions can share a short id prefix anyway. Nothing depends on the filename identifying the session, because the provenance marker inside the file already does that.
Then keep the no-clobber check as a backstop: if the path somehow exists, generate a new token rather than overwriting. The random token is what makes a collision improbable; refusing to overwrite is what keeps an improbable one from costing a handoff. Second-resolution timestamps matter for the same reason: coarse stamps widen the window, and similar concurrent tasks produce similar slugs.
Resolve `<primary-checkout>` explicitly — it is NOT necessarily where you are standing. If this session is running in a linked git worktree, the obvious answers (git rev-parse --show-toplevel, or the cwd) give the worktree's root — and worktrees are routinely deleted when the task that created them ends, taking the one artifact this skill exists to produce with them. A handoff that dies with its worktree is worse than no handoff: the session reports success and leaves nothing behind. Resolve it:
git worktree list --porcelain | head -1 | sed 's/^worktree //'The first entry is always the primary checkout. In an ordinary clone it returns that same checkout, so run it unconditionally rather than branching on whether you think you're in a worktree. (In a bare-repo + worktrees setup the first entry is the bare .git directory, marked bare in the output — that has no checkout to prefer, and it is the one location there that outlives every worktree, so it is still the right target. Don't be surprised by a path ending in .git.) State the absolute path you wrote to in your closing message — when the destination is not the directory the session has been working in, that is precisely what the operator needs to see, not a silent redirect.
On committing: don't — and you don't need to. Durability here comes from location, not from committing: the primary checkout outlives worktree cleanup, so the read-only gate stands. Whether handoffs are tracked is the project's call and genuinely varies — some repos commit their handoff history, others gitignore .claude/ wholesale, and a handoff written under an ignored path is still durable, just untracked. Report which you observed and leave the decision to the project; a declared close-out contract (Step 4b) is the one thing that can authorize more.
Run that check from the checkout you wrote to, not from where you're standing:
git -C <primary-checkout> check-ignore -v .claude/handoffs/<file>Bare git check-ignore -v <absolute path> from inside a worktree exits 128 with "is outside repository", because the path belongs to a different checkout than the cwd. Exit 0 means ignored, 1 means not ignored, and 128 means you asked the wrong repo. (If the primary is a bare repo, there is no worktree to test against and no tracking question to answer; skip the check and say so.)
Stamp a provenance marker as the very FIRST line of the handoff file (before the H1), so a later automated review can identify the handoff THIS session wrote and never a concurrent sibling session's: <!-- review-loop:session:<session-id> -->. <session-id> is the current Claude Code session id (the UUID for this session — e.g. the active transcript's id for this cwd, or a --session-id value surfaced in this session). If you cannot determine it with confidence, write <!-- review-loop:session:unattributed --> — the review will then safely SKIP reconciling this handoff rather than risk editing the wrong one. (HTML comments are invisible in rendered markdown.) This is the only hook the /review-loop "Reconcile the session's handoff" step keys on.
Stamp where the session ran, on the line directly after the provenance marker: <!-- session-end:origin branch=<current branch> worktree=<basename of the worktree root> -->. Writing to the primary checkout is what makes the handoff durable, but it also means sessions running in different worktrees now share one handoff directory. "The newest file" therefore stops being a safe way for a later session to tell whose handoff it is holding: end two concurrent sessions minutes apart and the newest belongs to whichever finished last, not to the branch being resumed. This line is what lets the resuming session disambiguate.
`worktree=` is the directory's BASENAME, never an absolute path. A handoff is durable and in many projects committed, so anything stamped here can end up in shared or public history — and an absolute path carries the operator's home directory, username, and often a client or project name. branch is what a resuming session matches on; the basename is only a tiebreak for the rare two-worktrees-one-branch case, and it buys that without persisting a home path. If even the basename is sensitive, drop the field and keep branch.
Some projects have a session acquire state at start that must be released at end: a claim on a shared desk or role, a lock, a lease, a "session N is live" marker. This skill cannot know what those are — but a claim left held by a session that no longer exists silently blocks the next one, and the symptom (a live-looking claim naming a dead session) points nowhere near the cause.
Look for a project-declared contract at <primary-checkout>/.claude/session-close-out.md. If it is absent, skip this step — never invent close-out actions a project did not ask for. If present, read it and do what it says. The writes it authorizes are a declared, scoped exception to the read-only gate — that is the contract's entire purpose, and it is bounded by what that file names and nothing wider.
If you cannot complete it, say so loudly — that is the whole job here. A close-out that silently half-ran is worse than one never attempted, because the project now believes it ran. Name the exact state left held, where it lives, and the command to clear it — in the handoff's Open threads section and in your closing message. An unreleasable claim the operator can see is a nuisance; one they cannot is a trap for the next session.
A close-out is a record, not a deploy — never block on CI. Releasing a claim, writing the handoff, and stamping what happened are all descriptions of work that already occurred, so nothing here waits on a build, a check run, or a merge queue. If CI is red or still running when you close out, say that in the handoff and release the claim anyway. A desk that holds its claim until a pipeline turns green hands the next session a live-looking lock on a dead process, which is the exact failure this step exists to prevent. The companion rule on the reviewing side is "post the signed verdict and stop" (docs/AUTO-MERGE-POLICY.md in Command).
Then go back and record what it changed. This step runs after the handoff is written, so a contract that succeeds mutates state the handoff has already described — the file it touched is missing from Artifacts, and any line saying the claim is held is now false. That is the same failure this skill exists to prevent, produced by the skill itself: a record that reads as current while describing a state that no longer exists. Whatever the contract changed is an artifact of this session, so amend the handoff to list it and to reflect the released state. Update on the success path, not only on failure.
Ask it once, of the session itself, never of the operator: did this session learn something another session would otherwise have to rediscover? That means knowledge, not state: how a repository deploys or breaks, a convention or trap, a decision and the reason for it, a pattern with evidence, or what a source (a study, a report, a book) establishes. What is in flight, a status or a count is not it; that belongs in the project's own goals or NEXT-ACTIONS. A rule about how Claude should behave is not it either; that is auto-memory.
wiki skill's learn procedure for it before closing: a draft, the pages, aclaims manifest, then node "$HOME/.claude/skills/wiki/lane.mjs" land. Put the pull request it prints in the closing message, and when a handoff is written, list it under Artifacts as wiki: <pull request>; the lane's own output is its evidence (see the gate below). If the lane refuses the write and the fix is not quick, name what was learned and why it did not land under Open threads instead, so the knowledge survives the session. On a light close there is no handoff to hold it, so write the full record instead: knowledge left only in chat is lost with the session.
If the session is DONE — nothing in flight (work shipped, parked, or merged) — skip this step. Close with the Step 2 record plus a one-line stop marker ("Nothing in flight; clean stopping point — no resume needed."). Don't manufacture a continuation prompt when there's nothing to continue; that's the whole point of ending vs handing off.
If work is mid-flight (Step 3 applied), emit a self-sufficient Claude Code prompt to start the next session. It MUST:
pasted prompt has no git log behind it. Stripping [derived] to make the prompt read cleanly is exactly how an inference becomes the next session's premise. Carry the tag, or the inline evidence.
a snapshot: commits land, branches move, and another session may have advanced or abandoned the work. The rules that catch that travel in the prompt, because a continued session starts from the pasted prompt and nothing else. (They used to live in a separate session-pickup skill, retired 2026-09-16 after a search of the operator's session logs counted 7 invocations of it against 25 continued sessions that opened with the pasted prompt instead [measured then, not re-checked here].)
Before acting, reconcile this prompt with current reality. The handoff was a snapshot.
1. Read the handoff at <absolute path>. Its second line names the branch it was written on. If that
is not the branch you are resuming, stop and ask me which handoff to use.
2. Run git status --short and git log --oneline -20, and check each file the handoff calls
in-progress: still there, changed since, or already landed?
3. Tell me which it is: unchanged, advanced (adjust the next step), or conflicts (stop and show me).
4. Treat every [derived], [assumed] and [unverified] claim as unchecked until you check it, above
all one that would justify repairing something. It may be deliberate.
5. Rebuild the todo list from the handoff, keep its settled decisions settled, honour any stop gate
it names, and confirm with me before a side-effecting step unless drift is unchanged and the
step is read-only.Present it in a fenced block, ready to paste. Keep it tight but complete — it is the single thing that determines whether the next session continues cleanly.
contract authorizes (Step 4b). No commits. Absent such a contract, no edits to other files. The Step 4c wiki lane is outside this rule: it writes and commits only in the wiki repository, through its own PR.
its absolute path is stated in the closing message.
git status/on disk, in the widened evidence of Step 1, **is thewiki pull request a Step 4c lane run printed (that output is its evidence; it lives in another repository), or is a change an executed close-out contract made* (Step 4b) — including a file it deleted*. Releasing a claim often means removing an untracked or ignored lock file, which then exists nowhere: not on disk, not in git status (deleting an ignored file leaves no status entry), not in any commit. The executed contract is the evidence for those, and without this carve-out the gate would force you to drop the close-out from the record or fail your own rule.
source of Y" · "the reason is…" · "this reads the…" · "that's a bug/divergence/leftover" · "should be fixed". For each, ask: did I check this, or did I conclude it?* Concluded → [derived] (or go check it now). This pass is cheap and catches the review-pr failure above.
The next session resumes from the continuation prompt this emits; its reconcile block (Step 5) carries what the retired session-pickup skill used to do. pattern-retrospective (rigorous multi-session studies) and chat-history-search (find past prompts) are heavier and backward-looking. session-end is the fast, forward-looking close-out for this session — recording it always, and handing it off when there's work to continue.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.