SLOPSHOPPER

handoff

One-click handoff: asks Claude to write handoff.md, waits until it exists, runs /clear, then has the fresh chat read handoff.md and continue. /handoff, or the…

newcommandtimer
★ 1v0.1.0no licenseupdated 2026-10-11erte-del/claude-mods/mods/handoff
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · handoff
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /handoff ⎿ handoff: handoff: asked Claude to write /work/app/handoff.md; will /clear and continue once it exists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-mods

<img width="391" height="379" alt="image" src="https://github.com/user-attachments/assets/6a3cb2d9-57cb-47e2-84c0-6cfc547499c3" />

Small mods for Claude Code: a live sidebar with one-click buttons for the things you do every session, and a repo picker. Works on Windows, macOS and Linux.

ModWhat you get
sidebarModel badge, context-window gauge, session cost, a 5-hour / weekly usage dial, and buttons for New chat, Commit & Push and Handoff
commit-pushOne click to stage, commit (with a Claude-written message) and push
handoffOne click to save a handoff.md, clear the chat, and continue from it
repoPick a GitHub repo, clone it if needed, and move the session into it

Install

In Claude Code:

/plugin marketplace add erte-del/claude-mods
/plugin install sidebar@claude-mods
/plugin install repo@claude-mods
/reload-plugins

Or from a terminal:

claude plugin marketplace add erte-del/claude-mods
claude plugin install sidebar@claude-mods --scope user
claude plugin install repo@claude-mods --scope user

Installing sidebar also installs commit-push and handoff, because its buttons call them. repo is separate, since it opens a picker when a session starts; skip it if you don't want that.

Start a new session or run /reload-plugins and the sidebar appears. It docks beside the transcript on terminals 110 columns or wider, and otherwise shows as a bar above the prompt.

Requirements: Claude Code and git. repo also uses the GitHub CLI (gh, signed in with gh auth login) to list your repos.

What each mod does

sidebar

Live status, redrawn after every turn:

  • the current model
  • how full the context window is
  • what the session has cost so far
  • how much of your 5-hour and weekly limits you've used, as a dial, with time until each resets

Buttons, top to bottom: + New chat runs /clear. ⇡ Commit & Push and ⇄ Handoff run the mods below. ↻ Reload plugins runs /reload-plugins.

commit-push

Click ⇡ Commit & Push, or type /commit-push.

  1. Checks the current repo. If nothing changed, it says so and stops.
  2. Stages everything (git add -A).
  3. Sends the staged diff to Claude (Haiku) for a one-line conventional-commit message such as fix: handle empty repo list in picker. If that fails it falls back to naming the changed files.
  4. Commits, then pushes. If the branch has no upstream yet, it runs git push -u origin <branch>.
  5. Shows the commit message and the result in the sidebar. A failed push shows git's own error text plus a hint (network, sign-in, rejected, protected branch).

It never force-pushes. It stops and leaves it to you when the branch is behind its upstream, there are unresolved merge conflicts, a merge or rebase is in progress, or HEAD is detached. Your files are only staged when it gets as far as committing.

It works on whatever folder the session is in, so it follows repo.

handoff

Click ⇄ Handoff, or type /handoff. Use it when your context is getting full.

  1. Asks Claude to write handoff.md in the project root: what you're doing, current state, the exact next step, decisions made, gotchas and open questions. An existing handoff.md is overwritten.
  2. Waits for the turn to finish and the file to appear.
  3. Runs /clear.
  4. Tells the fresh chat to read handoff.md and carry on from its next step.

If you interrupt, the turn errors, or Claude finishes without writing the file, it stops without clearing anything. The prompt is in mods/handoff/hooks/register.tsx if you want to change what goes in the file.

repo

Opens a picker when a session starts with no repo chosen, or type /repo.

  • Type part of a name to fuzzy-search your repos (matches on name, then description), list to see them all, then a number to pick.
  • Or type a full name, owner/repo, or a git URL.
  • It looks for an existing checkout in the usual places (~/repos, ~/Documents, ~/Desktop, ~/code, ~/projects, ~/dev, ~/src, ~/GitHub and a few more) and clones the repo if it can't find one.
  • Then it moves the session into that folder and tells Claude which repo is active.

Settings

Change these in /config (each mod's settings are listed under its name).

ModSettingDefaultWhat it does
sidebarbudgetUsd5The session cost that fills the cost bar
sidebarasciioffDraw bars with # and - and skip the dial, for terminals that can't show block characters
repoowneryour gh loginWhose repos to list: another user or an organization
repocloneDir~/reposWhere missing repos are cloned
repoopenOnStartonOpen the picker when a session starts with no active repo

Updating and removing

claude plugin marketplace update claude-mods
claude plugin uninstall sidebar@claude-mods

Each mod lives in its own folder under mods/ with its tests next to the code (claude plugin test mods/<name>).

Source 2 files
hooks/register.tsx 104 lines
1import { atom, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { HandoffRun } from '../types'
5
6export const IDLE: HandoffRun = { phase: 'idle', summary: '', path: '' }
7
8const run = atom({ plugin: 'handoff', key: 'run' } as const, IDLE)
9
10export const PROMPT = `My context is getting full. Before we stop, write a handoff.md in the project root so a new session with zero memory of this conversation can pick up exactly where we left off. If handoff.md already exists, overwrite it completely — don't append, this one replaces it. Keep it short and concrete, not a full doc. Include:
11
12What we're doing: the task/feature in one or two sentences.
13Current state: what's done, what's half-done, what's broken right now.
14Next step: the exact next thing to do, specific enough to act on immediately.
15Decisions made: anything we chose along the way that isn't obvious from the code (and why, if it matters).
16Gotchas: anything that tripped us up or that the next session needs to know to avoid repeating a mistake.
17Open questions: anything still unresolved.
18Don't pad it or restate things that are obvious from reading the code. If something's already clear from the repo, skip it.`
19
20export const continuePrompt = (path: string) =>
21  `Read ${path} — it is the handoff from the previous session, which had to be cleared because its context was full. Then continue exactly where it left off, starting with its "Next step".`
22
23// Turns that may end before ours does (a turn already running when the button was pressed).
24const MAX_MISSES = 2
25
26// The flow in flight; module-level so a /clear in the middle of it doesn't lose it.
27let pending: { since: number; paths: string[]; misses: number } | null = null
28
29const join = (dir: string, name: string) => `${dir.replace(/[\\/]+$/, '')}${dir.includes('\\') ? '\\' : '/'}${name}`
30
31async function set($: any, next: HandoffRun): Promise<void> {
32  await update($, run, () => next)
33}
34
35// The handoff.md written since `since`, if any (project root first, then cwd).
36async function written($: any, paths: string[], since: number): Promise<string | null> {
37  for (const p of paths) {
38    try {
39      const s = await $.fs.stat(p)
40      // a little slack for file-system timestamp rounding
41      if (s.kind === 'file' && s.size > 0 && s.mtimeMs >= since - 2000) return p
42    } catch {
43      // not there
44    }
45  }
46  return null
47}
48
49async function start($: any): Promise<string> {
50  if (pending) return 'handoff: already in progress, waiting for handoff.md'
51  const root: string = await $.session.root()
52  const cwd: string = await $.session.cwd()
53  const paths = [...new Set([join(root, 'handoff.md'), join(cwd, 'handoff.md')])]
54  pending = { since: await $.clock.now(), paths, misses: 0 }
55  await set($, { phase: 'writing', summary: 'Claude is writing handoff.md…', path: paths[0] as string })
56  // A command hook can't submit (the submit would wait on the turn the hook holds), so a timer does.
57  $.clock.after(1, () => void $.prompt.submit({ text: PROMPT, asUser: true }))
58  return `handoff: asked Claude to write ${paths[0]}; will /clear and continue once it exists`
59}
60
61// /clear, then hand the fresh chat the file. Runs after the turn that wrote it has ended.
62async function clearAndContinue($: any, path: string): Promise<void> {
63  try {
64    await set($, { phase: 'clearing', summary: 'handoff.md written, clearing…', path })
65    await $.command.run({ command: 'clear' })
66    await $.prompt.submit({ text: continuePrompt(path), asUser: true })
67    await set($, { phase: 'done', summary: 'New chat is continuing from handoff.md', path })
68  } catch (err: any) {
69    await set($, { phase: 'error', summary: `handoff: ${String(err?.message ?? err)}`, path })
70  }
71}
72
73export const register: Register = on => {
74  on('session.start', async ($, e, next) => {
75    await $.command.register({
76      name: 'handoff',
77      description: 'Have Claude write handoff.md, then /clear and continue from it in a fresh chat',
78    })
79    return next(e)
80  })
81
82  on('command.run', { command: 'handoff' }, async $ => ({ text: await start($) }))
83
84  on('turn.complete', async ($, e, next) => {
85    const result = await next(e)
86    const p = pending
87    if (!p) return result
88    const path = await written($, p.paths, p.since)
89    if (path) {
90      pending = null
91      // on a timer: /clear can't run inside the turn's own completion
92      $.clock.after(300, () => void clearAndContinue($, path))
93    } else if (e.reason === 'aborted' || e.reason === 'error' || e.reason === 'refusal') {
94      pending = null
95      await set($, { phase: 'error', summary: `Handoff stopped: the turn ended (${e.reason}) before handoff.md was written`, path: p.paths[0] as string })
96    } else if (++p.misses >= MAX_MISSES) {
97      pending = null
98      await set($, { phase: 'error', summary: 'Handoff stopped: Claude finished without writing handoff.md', path: p.paths[0] as string })
99    }
100    return result
101  })
102}
103
104
types/index.d.ts 14 lines
1export type HandoffPhase = 'idle' | 'writing' | 'clearing' | 'done' | 'error'
2
3export type HandoffRun = {
4  phase: HandoffPhase
5  summary: string // one line shown in the sidebar
6  path: string // handoff.md being waited on
7}
8
9declare module 'claude-code' {
10  interface PluginState {
11    handoff: { run: HandoffRun }
12  }
13}
14