Skip to content

feat(session-handoffs): end-of-day handoff notes from every running Claude Code session - #13

Open
lhoupert wants to merge 31 commits into
mainfrom
feat/session-handoffs
Open

lhoupert wants to merge 31 commits into
mainfrom
feat/session-handoffs

Conversation

@lhoupert

@lhoupert lhoupert commented Sep 29, 2026 •

Copy link
Copy Markdown

Adds session-handoffs, a Claude Code plugin for people who run several sessions at once. At the end of the day it reads each session's transcript to see which ones left a current handoff note and lists them under a "For Tomorrow" heading of a daily note. For busy sessions with no note, it asks once whether to write a snapshot from each transcript, in a subagent each, or to ask sessions idle for under an hour to write their own. It never waits on another session, and a re-run doesn't bring back lines you deleted. Tested with python3 -m unittest discover -s skills/session-handoffs/scripts (89 tests, in four time zones and on Python 3.9), claude plugin validate, and read-only replays of ten days of real sessions.

For reviewers: Claude Code only. The check reads Claude Code's transcripts (.jsonl), which are not a documented interface, and exits with an error when it can't read them instead of printing an empty report. A session counts as running if claude agents --json or ~/.claude/sessions/*.json (also undocumented) lists it. The files are always read, because inside Claude Code's Bash sandbox the CLI lists only some sessions. The opt-in hooks from #14 are merged into this branch.

Author attestation

  • I am a human, these are my changes, and I have reviewed and understood every change and can explain why each is correct.

AI-assisted: Claude Code wrote the skill, script and tests. Since the first review, it addressed the review comments and the duplicate lines a live run left in the daily note. It then went through a /code-review max (15 findings) and two rounds of independent verification. Every script rule has a test that fails without it, and a ponytail pass was applied. The claude agents --json change (995fbdb) had a /code-review low and a live run with and without the sandbox. Its follow-up (a22b91a), which reads both lists after review, had a /code-review medium, a mutation check, and the same live run, with matching rows.

🤖 Generated with Claude Code

lhoupert and others added 4 commits September 28, 2026 19:53
…n (1.0.0)

New Claude Code plugin. It checks which running sessions already left a
handoff note today (read-only, from the local session registry and
transcripts), asks only the others to write one once they are idle, and
lists every note under the daily note's "For Tomorrow" heading.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ph9PzkBrREkNY1v4C8GWM7
From the first live run:
- the check drops notes that open with a SUPERSEDED banner (after any
  frontmatter), so a replaced note is no longer listed next to its successor
- it prints each note's title (first heading, else file name); the skill
  shortens it and writes one plain line per note
- notes named for a later day count, older compact dates (20260903) don't
- a stale session that never replies still gets its notes listed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ph9PzkBrREkNY1v4C8GWM7
…cript

- `handoff_status.py --digest NAME` prints the session's latest note and a
  compact log of its day (prompts, Claude's messages, one line per tool
  call, no tool output, secrets masked, capped at the most recent 25k chars)
- SKILL step 5 / `--from-transcript`: on request, write a labelled snapshot
  note for a session that stays busy

Fixes from a max-effort review of the whole skill:
- `--date` is the start of the window, so a run after midnight sees the evening
- notes in any `git worktree add` checkout are not durable, not just .claude/worktrees
- a date in a note's name counts only up to 3 days ahead (deadlines don't)
- frontmatter is read to its closing `---`
- digest skips skill bodies and compaction summaries, labels peer messages
- one shared transcript reader; tests pass with the temp dir under $HOME

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ph9PzkBrREkNY1v4C8GWM7
Live runs showed the subscribe-and-wait flow stalls: a session running
background work never counts as idle, the coordinator had to stay open,
and closing it dropped every pending request. An architecture review
(mechanics, two adversarial passes, alternatives, cost) recommended:

- /session-handoffs only reads: check, write the daily list, report. It
  can be re-run any time; closing it early loses nothing.
- `--ask NAME` sends one request to an idle session, with no waiting;
  `--from-transcript NAME|SID8` writes a snapshot note, which counts for
  the session it describes.
- Running sessions come from the registry; this session is left out via
  $CLAUDE_CODE_SESSION_ID. Automation (SDK, claude -p) is not reported.
- Out of date = more than 30 main-conversation calls or subagent file
  edits after the latest usable note; sessions without a note and under
  30 calls are left out; new closed-stale verdict.
- Listing is its own rule (date window, SUPERSEDED banner incl. dated
  and quoted forms), so a replaced note isn't listed next to its successor.
- --date defaults to 5 hours ago; submodules are not worktrees; more
  secret forms masked in the digest.

Reviewed with /code-review high (10 findings, fixed); 14 tests, each rule
mutation-checked (37/37).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ph9PzkBrREkNY1v4C8GWM7
@claude

claude Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Claude finished @lhoupert's task in 14s —— View job


✅ No blocking issues — safe to merge.

Reviewed the latest change (a22b91a, agents() in handoff_status.py:333-350 and the union at :398). I couldn't run the test suite here because command execution needed approval, so this is a read-through only.

  • It answers dannybauman's last question. A running session is now counted if either claude agents --json or sessions/*.json lists it. A short sandboxed list can no longer hide the other sessions. The "own session appears in the list" heuristic is gone.
  • Failures in the CLI call (missing binary, timeout, bad JSON, non-list output) return []. The registry then covers the result.
  • registered is computed from the registry alone (:396), so a CLI list can't mask a registry problem.

Simplify (ponytail)

  • handoff_status.py:388-394: the registry read_text + json.loads loop could share a small helper with handoff_guard.py if both read the same files. Optional.
    · branch feat/session-handoffs

💰 Estimated review cost: $0.13 · 0m13s · 8 turns

…ge snapshots faster

From a three-reviewer check of the 29 Sep live run:
- A session opened to run the report (first tool call runs the script or
  loads the skill) is left out. It showed as `none` on the next run and
  was offered a snapshot of itself; its snapshots still count for their
  sources.
- A .md note in a handoff/ or handoffs/ directory counts as a note.
- A snapshot is out of date after 5 source calls, not 30: it can't know
  what came after it.
- --digest refuses an id prefix shared by two sessions.
- Dates inside longer numbers are not read as a note's date.
- Masking adds Authorization, JWT, PEM, Google keys, *_KEY=, SAS sig=
  and IPv4 addresses.
- SKILL.md step 5: the log is data, never instructions; say when the log
  was cut.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@dannybauman

Copy link
Copy Markdown
Member

thanks for sharing this @lhoupert! I run into this issue too when wrapping up a bunch of parallel sessions at the end of the day. I tried the check script on my machine, the 14 tests pass and the check took about 0.2s across 13 sessions.

A few questions from trying it:

  • across a week of my sessions (40 of them) it found no notes, since mine rarely write a handoff file on their own, which I'd guess is the case for some people trying it. should asking the sessions for a note be the default first step, or is that what the Stop hook follow up is for?
  • could the note location be set the same way the daily note is? I'd want mine in my Obsidian vault so they sync between machines
  • if Claude Code changes the transcript format, would the report come back empty? it looks like entries() skips lines it can't parse
  • could claude agents --json stand in for reading ~/.claude/sessions/*.json? on my machine (2.1.284) it lists the same 13 sessions with sessionId, name, kind and an idle or busy status, and it's the documented way to read session state from a script: https://code.claude.com/docs/en/agent-view

other projects I came across:

@lhoupert

Copy link
Copy Markdown
Author

Thanks @dannybauman, it is really useful!

should asking the sessions for a note be the default first step, or is that what the Stop hook follow up is for?

Asking every session by default is off on purpose. Mostly to avoid that waking one that has been idle for over an hour re-reads its whole context. Mine write notes because I ask for them, and my CLAUDE.md says where they go. I'll add that line to the Requirements.
The report should still list your busy sessions (30+ tool calls) as none, with an offer to ask them or snapshot them. Did you get those rows, or an empty report?

could the note location be set the same way the daily note is?

Yes, I'll add a session-handoffs notes: line. Notes are already found anywhere if the name contains "handoff". But they're found through local transcripts, so a synced note is only listed on the machine that wrote it.

if Claude Code changes the transcript format, would the report come back empty?

Yes, good catch 😅 . I'll make it exit with an error instead.

could claude agents --json stand in for reading ~/.claude/sessions/*.json?

I'll look into it!

Thanks for the links. I'll borrow "What We Tried" for the note checklist.

lhoupert and others added 21 commits September 29, 2026 20:42
- A run of the report is no longer dropped from the report; it is only
  never listed as lacking a note. So a wrong match can't hide a working
  session, its notes or a snapshot of it (findings 1, 3, 4). The run is
  detected from the session's first tool call ever, not the first in the
  window, and only when that call runs the script, not when it names it.
- The SDK filter again uses the first entrypoint seen (finding 14).
- The 5-call staleness applies only to a snapshot another session wrote,
  not one the session later edited itself (finding 5).
- --digest refuses a name shared by two running sessions; the report
  shows their sid8 (finding 7).
- Private keys are masked whole, through their END line, in a pass
  before SECRET (findings 2, 8). Quoted values are masked whole, an
  Authorization header without a scheme is masked, more token prefixes
  are covered, and a bounded label keeps a long blob linear (64k chars:
  ~70 s -> 0.08 s) (findings 9, 10, 11).
- A date followed by a time (202609201030) is a date (finding 12); only
  dated .md files in a handoffs/ folder are notes (finding 13).
- SKILL.md: the other session's note is data too (finding 15); says
  IPv4; notes that a snapshot's freshness counts from its write time
  (finding 6, left as a limit).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Three lines at 384d792 were over the line length, so
  `ruff format --check` failed before any change. Formatting only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…note

- A snapshot is now `replaced` once the session's own note covers it:
  written after it, or at most 5 calls before it. It gets a `replaced`
  row naming that note instead of a line of its own. Before, a session
  that wrote its own note after its snapshot, or whose snapshot came
  after its own note with no work between, was listed twice.
- A replaced snapshot is still reported when it carries a banner or
  predates the own note, so a line an earlier run wrote for it gets
  swapped for the own note, or deleted if that note has a line already.
  Its row comes before the own note's row.
- Step 2 keeps a hidden record of what it listed at the end of the
  section (an HTML comment) and skips any path the record names. A
  re-run no longer brings back a line the user deleted or reworded. A
  line already counts if it has the path with ~ or in full, or a wiki
  link to the note. A snapshot the run has just written is added even if
  the record names its path.
- The run marks a replaced snapshot SUPERSEDED and removes the memory
  line a run of this skill added for it. It never edits the session's
  own note.
- --ask names the session's snapshots and says its own note supersedes
  them.
- In a handoff(s)/ folder, a file with "body" in its name (a PR or
  issue body) is not a note. A note that opens with a quoted
  `> COMPANION of <path>` line (one kept on purpose beside another) is
  not listed. Plain "Companion notes: ..." lines still are.
- A note's title is its level-1 heading, else its file name. A section
  heading ("## Status ...") is no longer taken as the title.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…'t be read

- A renamed transcript field gave a header-only report and exit 0. The
  check now exits 1 with a one-line message when projects/ is missing,
  when registry files exist but none yields a sessionId (a renamed key,
  or a file that is no longer a JSON object), or when transcripts
  changed in the window but no tool call could be read from any of
  them.
- The tool-call count includes the session running the report, whose
  own call to the script is always in the window, so a quiet day, an
  empty day or a new install still exits 0.
- --digest exits 1 when the log is empty instead of printing a blank
  log that would become an empty snapshot.
- claude agents --json is not used: it is documented, but lists no
  sessions inside Claude Code's Bash sandbox, where this script runs.
  One Limits sentence and a code comment say so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
…choose

- A `session-handoffs notes: <dir>` line, set like the daily-note line,
  names a folder for notes (for example in a synced notes vault).
  Without it nothing changes.
- --digest takes --notes-dir and prints the snapshot path in that
  folder. It expands a quoted ~, since the model quotes the path.
- --digest stops with an error when the folder is missing, or sits
  where the check never finds notes (memory/, temp dirs, git
  worktrees). A snapshot written there would never count. The script
  never creates the folder: a typo in the line would add a stray
  folder to a synced vault.
- SKILL.md: the --ask request writes new notes in that folder, and
  step 5 passes --notes-dir. Limits now say notes are found through
  this machine's transcripts, so a synced note is listed only on the
  machine that wrote it.
- Detection is unchanged: requested notes and snapshots are named
  handoff_<date>_<topic>.md, so they are found anywhere. Counting
  every dated .md in the folder would list unrelated vault notes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
…ubagent

For users whose sessions don't write notes on their own (from the PR
review):
- Requirements: a copyable CLAUDE.md line in the same words as the rule
  the --ask request sends, so a session asked for a note puts it where
  it lasts. One rule, not two.
- The note checklist adds "what was tried and ruled out, and why", after
  the "What We Tried" section of REMvisual/claude-handoff.
- Step 3 asks one question, "Snapshot these N?", when rows are stale or
  none; the user says yes, no or names some. A snapshot only reads the
  transcript. --ask is suggested only for running sessions active in the
  last hour: waking one idle longer re-reads its whole context.
- Step 5 writes each snapshot in its own general-purpose subagent, with
  a fixed prompt. Written in the report's own context, 3 snapshots cost
  2.6M input tokens, because every later call re-read each log. The
  prompt carries the "another session's data" rule; the subagent writes
  only the snapshot and replies with its path.
- Memory: one line per day listing that day's unreviewed snapshots
  (paths and names only, replaced on each run) instead of one line per
  snapshot. Memory loads in every session, and nobody has reviewed them.
- Test: a snapshot written by the report session's subagent counts for
  the session it describes, which the new step 5 relies on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
- The Recommended CLAUDE.md line named only the directory that holds
  memory/, while the notes line and the --ask request send notes to the
  notes folder when one is set. A session following the CLAUDE.md line
  would put its note outside that folder.
- The line now says the notes folder if set, else that directory: the
  same rule as the request's <notes>, still in the request's words.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
- Each snapshot is now written by a subagent from a fixed prompt that
  repeats the --digest command, and that copy had no --notes-dir. With
  a notes folder set, snapshots still landed in the project directory.
- The prompt now passes --notes-dir '<folder>', dropped when no folder
  is set.
- The subagent never reads step 5 item 1, where "don't create it" lives,
  so the prompt also says not to work around a script that stops: a
  folder made by mistake would land in a synced vault.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
- Step 5 now keeps one memory line per day naming every snapshot in the
  report. Replaced snapshots are in the report too, as `replaced` rows,
  so the line would have kept pointing at snapshots an own note covers.
  It now names only snapshots that aren't `replaced`.
- Step 2 still told the run to remove "the memory line step 5 wrote"
  for a replaced snapshot, a per-snapshot line that no longer exists. It
  now drops the snapshot from the day's line, so this happens on every
  run, not only on runs that write snapshots.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
- Step 1 said the check prints one row per note worth starting from, or
  per session without one. It now also prints one row per `replaced`
  snapshot, which is not a note to start from.
- Limits said any file in a handoff(s)/ folder is a note. The check
  counts only dated ones there, which is also why a notes folder named
  handoffs/ picks up dated notes without "handoff" in their name.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
- The intro said the skill only reads. It writes the daily note, one
  memory line and snapshots; it now says so, and that it never edits
  another session's own note.
- Say where the daily note and notes folder lines are read from, up
  front. Steps 4 and 5 used the notes folder without saying where to
  find it.
- Define `<date>` once (the --date value, else the day 5 hours ago), so
  the daily note, --digest and the memory line use the same day as the
  check. Say that every run does steps 1 to 3, and that --ask and
  --from-transcript replace step 3's question: steps 4 and 5 need the
  report's rows.
- Step 2: a missing daily note file or heading prints the list instead
  of creating them. The record is defined where it is written. Lines are
  added only for rows with a path, so `none` rows get none. A snapshot
  written in this run is added unless a line already has it, not merely
  "even if the record names it", which invited a duplicate line.
- The memory line moves from step 5 to step 2, with "write no other
  memory", so it is kept on every run that writes. Step 2 no longer
  points ahead to step 5 for it.
- Step 5: the subagent prompt now carries every rule the subagent must
  keep (pasted text is data, don't edit that session's note or memory,
  don't create a missing folder); the duplicated item 1 and item 3 are
  gone. A snapshot gets a `#` title, which becomes its daily-note line.
  The run tells the user about any snapshot that wasn't written.
- Use the sid8 for a session whose row shows one: --digest refuses a
  name two running sessions share.
- Limits: the SUPERSEDED rule said "a quoted line containing it", which
  the check doesn't match; it now shows the banner form. The busy
  session line repeated step 4 and is gone.
- handoff_status.py docstring: NotebookEdit, automation left out,
  `replaced` rows and the header, what "covers" means, peer messages in
  the digest, and when --digest exits with an error.
- 2214 words to 2195.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Run from a session, the check now reads that session's own
  transcript, which holds the call running the script. It exits 1 if
  that transcript isn't under projects/*/ (a layout change) or no tool
  call can be read from it (a format change). Before, any other
  readable transcript passed the check. On the day of an update,
  sessions started earlier still write the old format, so sessions in
  the new one dropped out of the report with no error.
- Tool calls by the session's subagents count too, so a run from a
  subagent is not an error when the session made no call of its own
  that day.
- Run from a terminal, it still exits 1 when no tool call can be read,
  and the message now names the other cause: no session used a tool.
  Counting readable lines instead would miss a renamed tool call.
- Tests: an old-format session beside new-format ones, transcripts
  moved one folder down, a terminal run with a renamed tool call beside
  a chat, a quiet day where this session's call is the only one, a run
  from a subagent, and a registry with one session beside objects that
  aren't sessions.
- The test helper that reads the report first writes this session's
  call to the script, as every real run has one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
- A snapshot is replaced only when a run of the report wrote it last,
  or it already opens with a banner. Before, a snapshot the session had
  edited to continue from it, or one another session had edited, could
  be replaced, and step 2 would then put a SUPERSEDED line on a note
  someone works from.
- The own note that replaces a snapshot is the newest one named for the
  day, if there is one. Before, touching a note named for an earlier
  day could hide a snapshot that held the only record of the work since
  the day's own note.
- Tests pin which own note replaces a snapshot: the newest, not the
  oldest, and only one that exists, not a Write that never landed. They
  also pin the limits of the COMPANION banner: a quoted "Companion
  notes:" list, an unquoted "Companion of" and "companion of" inside a
  quoted line are not banners.
- A comment and the Limits say that "body" matches anywhere in a name
  in handoffs/, and that other drafts kept there count as notes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
- The check's first line now gives the day it covers and the time now,
  and SKILL.md takes <date> from it. The model knows today's date but
  not the clock. After midnight it could write to the wrong daily note
  and pass the wrong --date to each snapshot subagent, whose --digest
  then fails for a session last active before midnight.
- Step 3 checks "active in the last hour" against the time on that
  line instead of guessing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
…3 and 5

- Step 2: a replaced snapshot's line is deleted only while it is still
  as the skill wrote it. A line the user ticked or edited gets the
  note's path instead, and keeps their text.
- Step 2: drop "(out of date)" from a line whose row is now fresh or
  closed. A note updated in place kept that marker for good.
- Step 2: read only the first 5 lines of a replaced snapshot before
  marking it. The rest is another session's data, and no rule in the
  running session said so.
- Step 2: the memory line is one rolling line, replaced whatever date
  it names. A run sees only its own day's snapshots, so one line per
  day piled up in memory and was never pruned.
- Step 3: a row without a title shows its project and last_active, so
  the user can tell sessions apart when asked "Snapshot these N?".
- Step 5: the subagent covers the step 4 request's "Make it
  self-contained" bullet, not "the checklist": the request's other
  bullets say to edit that session's note and memory.
- Limits: the daily note keeps a hidden record of every path it listed,
  deleted lines too.
- SKILL.md goes from 2195 to 2338 words over the four commits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
- A snapshot an earlier run of the report wrote is replaced once the
  session's own note covers it. This is the case on real data: the
  snapshots come from a previous run, not from the session running now.
- A snapshot another session edited after the run wrote it is not
  replaced: someone works from it.
- Each test fails when its branch of the rule is removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
…dian-mind

- The note checklist asks for the user's corrections and preferences,
  next to what was tried and ruled out (claude-handoff's "User Feedback"
  and "What We Tried" sections). A log's user lines are where they live,
  and a summary tends to drop them.
- A snapshot says what changed since the session's last note.
- Step 2 banners a replaced snapshot only while it still opens with this
  skill's own "Snapshot written by" line, and only by inserting one line:
  a file someone else rewrote is left alone (obsidian-mind edits only
  files it wrote, and only by adding).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
…check

- Finding 1: a snapshot counts for the session it describes only when
  a run of the report, or the session running it, wrote it. Another
  session's Write or Edit of it stays that session's own note. Before,
  that edit moved to the described session: the editor showed as
  `none`, and the described session went on the 5-call rule.
- Finding 1: --digest exits with a message instead of printing a
  snapshot path that someone else wrote last, the described session
  included. Step 5 replaced that file and lost the edits of whoever
  works from it. SKILL.md says a session that edits a snapshot makes
  it its own note.
- Finding 8: a snapshot marked SUPERSEDED is `replaced` by the
  session's newest own note, however many calls separate them. The
  banner edit is the snapshot's last write, so a banner added more
  than 5 calls after the own note dropped its row, and step 2 never
  swapped its line.
- Finding 9: --notes-dir must be absolute after ~ expansion. An empty
  or relative folder printed a relative snapshot path, which landed
  under the subagent's working directory, where the check never finds
  it.
- Finding 14: the registry check stops only the report. --digest finds
  a session by its id and doesn't need the registry. Limits says a
  registry that moves isn't noticed.
- A session opened to run the report and then put to work still owns
  its edits of its own snapshot.
- Each new test fails with its change reverted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
- Finding 7: step 2 repoints, or deletes, only a line that holds a
  replaced snapshot. The note in its title is otherwise listed like
  any other row. "If its snapshot is listed, so is the note in its
  title" counted the title note as listed when no line held the
  snapshot any more, so a later own note never got a line. If the user
  deleted the snapshot's line, the own note's line is added once, and
  a deletion sticks after that.
- Step 2 no longer banners a snapshot marked "> COMPANION of": someone
  kept it on purpose beside another note.
- Finding 11: Limits says a banner opens the note after any
  frontmatter, as v1.0.0 did. A banner on line 1 turns YAML
  frontmatter into body text.
- SKILL.md goes from 2362 to 2389 words over the two commits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
…ck as the run's

- Finding 1's fix (V1-1, V3-R5): a snapshot counted for the session it
  describes only when the session that wrote it was opened for the
  report. A /session-handoffs run from a working session (its first
  call was other work) then kept its step-5 snapshots as its own
  notes: the snapshotted session showed as `none` again and was
  offered once more, and --digest refused to rewrite the snapshot on
  every run.
- A write counts as the run's once its session has run the check,
  in its main conversation or in a subagent (step 5's subagent runs
  --digest first). Its writes before that stay its own. --digest
  uses the same rule when it decides who wrote the snapshot last.
- V1-2: --digest no longer refuses a snapshot that opens with a
  SUPERSEDED banner, whoever wrote it last: the banner says nobody
  works from it. Before, the snapshotted session stayed `none` or
  `stale`, step 3 kept offering it, and step 5 failed all day. A
  COMPANION line still keeps it.
- Once a run writes over a snapshot, an earlier edit by another
  session no longer lists it as that session's note: the edit is gone
  from the file. Without this, the V1-2 change listed the rewritten
  snapshot under both sessions.
- SKILL.md says step 5 replaces a snapshot marked SUPERSEDED
  (2389 to 2394 words).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
…rote it

- The rule from 4d8877a ("the session ran the check before the write")
  still let step 5 overwrite a note: a session that ran the check at 11:00
  and then edited another session's snapshot at 13:00 had that edit
  counted as the run's, so --digest replaced it.
- Step 5 writes every snapshot from a subagent, so a write from a
  subagent transcript is a run's; a session's own conversation works from
  what it writes, before or after running the check. A session opened for
  the report, and the session running it, still count as runs.
- The mid-day run case behind 4d8877a still holds: its subagent's snapshot
  counts for the session it describes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
@lhoupert lhoupert changed the title feat: add a end-of-day handoffs skill from every running Claude session feat(session-handoffs): end-of-day handoff notes from every running Claude Code session Sep 30, 2026
lhoupert and others added 2 commits September 30, 2026 12:14
- A `claude --bg` session is registered with kind "bg" (entrypoint cli,
  a name and an idle/busy status), and ListAgents lists it, so
  SendMessage reaches it. The check counted only kind "interactive" as
  running, so such a session was reported closed, closed-stale or
  closed-none, and the report offered --from-transcript instead of --ask.
- LIVE_KINDS names both kinds; SKILL.md step 4 asks idle `bg` sessions
  too. Other kinds still aren't a working session.
- Reported by the /session-kickoff session, which starts one --bg session
  per focus item.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3
…te nudge

- scripts/handoff_guard.py runs on UserPromptSubmit, PostToolUse (Write)
  and Stop. It only reads this session's transcript, the notes and the
  memory files, and never writes a file.
- SESSION_HANDOFFS_GUARD=1: a prompt asking for a handoff (any spelling;
  not a path, a file name, pasted text or a /command) and a Write that
  creates a note get this session's other current notes, snapshots of
  it included, and the memory lines that name them, as context. At the
  end of that turn, a note named on a Supersedes or Replaces line of the
  new note that still has no banner comes back once as Stop hook
  feedback. Snapshots of the session are left to /session-handoffs,
  which banners them once the session's own note covers them.
- SESSION_HANDOFFS_NUDGE=1: for sessions that don't write notes. The
  first time a session has made 30 tool calls over 2 hours without a
  note, the user sees one systemMessage. Nothing is stored: it fires at
  the Stop where that holds and didn't at the previous Stop.
- A turn is everything since the previous Stop's stop_hook_summary entry,
  so a prompt, peer message or task notification queued mid-turn doesn't
  split it.
- Silent in subagents, plan and dontAsk modes, and `claude -p`/SDK runs
  the session registry doesn't list as interactive. Any error, SystemExit
  included, means exit 0 and no output; each hooks.json command ends in
  `|| true` (exit 2 on UserPromptSubmit erases the prompt) and runs
  `python3 -B`, so nothing is cached.
- Context is written as facts, not instructions; titles are shown only
  for notes this session wrote and that nobody changed since.
- About 20-40 ms per event on a 21 MB transcript: a turn end reads back
  only to the previous Stop.
- hooks/hooks.json wires it for plugin users, off unless a variable is
  set; SKILL.md gains "Optional: hooks" with a pinned-copy install for
  settings.json. Plugin version 1.0.0 -> 1.1.0, so installs pick it up.
- Tests: one per rule and per silent case, each failing with its rule
  removed, plus two that run the real script and parse its stdout as
  JSON.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8PczU1B2Y9DCWAV36fnH3

@dannybauman dannybauman left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @lhoupert. I pulled the branch, the tests pass and the check ran cleanly across my sessions today. the notes line is what I was hoping for, I'll point mine at my vault.

on claude agents --json, it did return my sessions from a Bash call, though some of my sessions run with permissions bypassed so it may not be sandboxed. could the check try it first and fall back to the session files when it comes back empty? fine as a follow up too

lhoupert and others added 2 commits October 2, 2026 09:53
feat(session-handoffs): opt-in hooks for one current note and a no-note nudge
… session

`claude agents --json` is the documented list of running sessions, but
inside Claude Code's Bash sandbox it lists only some of them (2 of 12 on
2.1.287, both `blocked` background jobs), so falling back only when it
is empty would drop sessions without an error. Trust it only when it
lists $CLAUDE_CODE_SESSION_ID, the session running the check; otherwise
read sessions/*.json as before. It calls `claude --bg` sessions
"background", so that kind counts as running too.
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 keeps it from calling
api.anthropic.com, which the sandbox blocks with a warning.

Suggested in review of #13.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@lhoupert

lhoupert commented Oct 2, 2026

Copy link
Copy Markdown
Author

Thanks @dannybauman, and for checking the Stop hook docs on #14!

I added one small change. Inside the sandbox, claude agents --json no longer comes back empty: it lists only 2 of my 12 sessions, so "fall back when empty" wouldn't catch it. Instead, the check looks for its own session in the list. If it's there, the list is complete and gets used. If not, the check reads sessions/*.json as before.

It came after your approval, so a quick look would be great if you have time!

@dannybauman

Copy link
Copy Markdown
Member

thanks @lhoupert, the change looks good to me. I ran claude agents --json here (2.1.287, sandbox off) and it listed the same 6 sessions as the session files.

one question: the check now trusts claude agents --json whenever its own session shows up in the list. in the sandbox, when it listed only 2 of your 12 sessions, could its own session be one of those 2? if so, it would use the short list and show the other 10 as closed. fine as a follow up

Inside Claude Code's Bash sandbox `claude agents --json` can list only
some sessions, and nothing stops the session running the check from
being one of them. Trusting the list whenever it names that session
would then show the others as closed. Read sessions/*.json every time
and add what the CLI lists, so a short list can add sessions but never
hide them. Whether the registry changed format is judged from the
registry alone, so a CLI list can't hide that either.

Raised in review of #13.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@lhoupert

lhoupert commented Oct 5, 2026

Copy link
Copy Markdown
Author

Thanks @dannybauman and good catch! Yes, you are right to ask, t could. a22b91a reads both now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants