SLOPSHOPPER

autodrive-handoff

Task snapshot that survives a /clear or /compact in Claude Code: the agent writes the current task to TASK.md and the open decisions and remaining todos to…

newtoastprocess
★ 2v0.18.0MITupdated 2026-10-15ddaanet/autodrive-handoff
A shopper browsing a rack in a slop shop
README

autodrive-handoff

Clear or compact a long Claude Code session without losing the thread.

Before a /clear or /compact, the plugin has Claude put the work in progress on disk: the open decisions and remaining to-do items, plus the task in hand before a /clear. After the reset the files are injected back, verbatim. If you ask, the plugin also runs the reset and resumes the work, so you type nothing.

What you get

  • A task frame that survives /clear. TASK.md and TODO.md in your project root, loaded into the next session.
  • A to-do list that survives /compact. The wrap-up saves TODO.md before the compaction and re-injects it word for word after.
  • Optional hands-off resets. Ask for "clear and continue" or "compact and continue" and the plugin runs the command and resumes from the files.
  • Optional commit. The wrap-up has Claude commit the two files with your work, or amend them into your last commit.
  • Plain files. You can read and edit them; the plugin has no database and no network code.

Install

/plugin marketplace add ddaanet/claude-plugins
/plugin install autodrive-handoff@ddaanet

Use it

Ask in plain words, or invoke a skill:

skilluse it
/autodrive-handoff:handoffbefore a /clear
/autodrive-handoff:precompactbefore a /compact
/autodrive-handoff:autonameto name the session, and nothing else
/autodrive-handoff:pendingto list the open decisions and remaining todos already loaded, with no tool calls

Examples of what to say:

You sayWhat happens
"save handoff", "wrap up"Files written and session renamed; you run /clear yourself.
"handoff and continue"Files written and session renamed, then cleared and resumed.
"handoff, commit"Files written, then committed together with the session's work.
"handoff, amend"For work already committed: the updated files are folded into that last commit.
"handoff, commit, continue"The commit, then the clear and the resume.
"precompact"Files written; you run /compact yourself.
"compact and continue"Files written, then /compact runs and the work resumes.

The commit is recognised by meaning, not by keyword: "handoff and ci" counts. Commit and amend combine with continuing in the same way.

What to expect

Clear. The wrap-up writes TASK.md (the task and where it stands, at most 1000 characters of prose) and TODO.md (open decisions and remaining items; finished items are dropped), and the session is renamed when your turn ends. With "continue", it is also cleared. The fresh session starts with both files injected, and the plugin submits one fixed prompt to resume.

Compact. The wrap-up writes TODO.md only; the compaction summary carries the task, so an old TASK.md is removed. Whatever must survive word for word goes in TODO.md. With "continue", /compact runs when your turn ends, TODO.md is re-injected and the work resumes.

Memory. If your repository uses gitlore, the wrap-up also offers to commit uncommitted memory: it drafts the message and waits for your approval before any clear or compact.

When it does not run. If the transition could not run, you are told at your next prompt, with the commands to run by hand. That happens on Claude Code older than 2.1.287 or under --safe-mode.

Finishing. Run the handoff again with nothing outstanding to clear both files. They are ordinary tracked files and ride your next commit.

Use compact when the work continues past a long session, and clear when the thread itself should restart. Compacting right before /clear wastes the compaction.

Limitations

  • One session per project root at a time. Sessions sharing a root share TASK.md, TODO.md and .claude/autodrive, unlocked, and the later wrap-up wins. Give parallel sessions a git worktree each.
  • Do not share one .claude/ between roots.
  • Another handoff plugin (ddaa-handoff, ddaa-passation) answers the same phrases. Enable one handoff provider per project.

Requirements

  • Claude Code 2.1.287 or newer (the plugin's mod runs the transition)
  • python3 and jq in $PATH

What the plugin runs and writes

Everything the plugin starts, submits and writes, checked against its code. It has no network code. Text that reaches the conversation, such as the injected TASK.md and TODO.md, is sent to the model like any other context.

Programs it starts

No binary is shipped. Every program is bash, python3 or jq over the plugin's own scripts/ and bin/, plus coreutils and read-only git.

WhereProgramPurpose
SessionStart hooksbash scripts/session-pointer.sh, load-handoff.sh, load-compact.shinject the files, clean up
UserPromptSubmit hookbash scripts/report-drive-failure.shreport a failed or stale transition, note session-root drift
`PreToolUse(Write\Edit)` hookbash scripts/write-guard.shdeny writes to .claude/autodrive
PreToolUse(Bash) hookbash scripts/inject-checkpoint-root.shsee below
Mod, at turn endbash <plugin>/scripts/drive-take.sh <cwd> <session-id>take the armed transition
Agent's Bash toolhandoff-checkpoint, handoff-approvedwrite the files; release a held transition

The mod skips drive-take.sh only when the turn ended in the session root and .claude/autodrive does not exist. Otherwise it runs it, and it prints nothing when no transition is armed for this session. The mod runs no git or network code.

git is read-only and runs only inside handoff-checkpoint, to learn whether a gitlore memory submodule has uncommitted changes: git config --file .gitmodules submodule.gitlore-memory.path, then git -C <memory> status --porcelain and git -C <root> config --get gitlore.memoryApprovalClauseFile.

What the mod submits

The mod runs only what .claude/autodrive holds, and only for the session that file names as its owner. The lines it can hold:

  • /rename <title>
  • /clear
  • /compact, or /compact <focus> with a one-line focus directive
  • the one prompt the plugin ever submits:
  Continue where the previous context stopped.

The mod checks the prompt box before each line and stops if you typed anything. A rejected line stops the sequence and is reported at your next prompt. The file's reader refuses any other slash command.

The PreToolUse(Bash) rewrite

When a Bash command contains handoff-checkpoint or handoff-approved, the hook rewrites it to export HANDOFF_ROOT=<root>; <original command>. It makes no permission decision: your usual permission rules apply. Other commands are left alone.

Because the rewritten command starts with export, a prefix rule for the command name alone does not match it, and you are prompted (a headless -p run is denied). To run the checkpoint without a prompt, allow all three in .claude/settings.json:

{
  "permissions": {
    "allow": [
      "Bash(handoff-checkpoint:*)",
      "Bash(handoff-approved:*)",
      "Bash(export HANDOFF_ROOT=*)"
    ]
  }
}

Files it writes

FilePurpose
TASK.md, TODO.md (project root)the task frame; plain tracked files
.claude/created by handoff-checkpoint when absent
.claude/autodrivethe transition to carry out; written only by handoff-checkpoint, direct writes denied by the `PreToolUse(Write\Edit)` guard
.claude/autodrive.failedwritten by the mod when a line is rejected; consumed at your next prompt
.claude/autodrive.sweptone line per stale transition file removed; last 50 kept
.claude/handoff-todo.md, .claude/handoff-task.mdfiles from an earlier version of the plugin; handoff-checkpoint moves the todo to TODO.md or removes either, and nothing else touches them
/tmp/claude/handoff-drift-<session id>marker so a session-root drift note appears once; /tmp/claude is created mode 0700 and markers older than 7 days are deleted

TASK.md, TODO.md, .claude/autodrive and .claude/autodrive.swept are each written through a temporary sibling file renamed over the name.

It also reads, never writes, the modification time of the owning session's transcript, to tell a live session from a dead one.

What it does not do

  • No network call.
  • No git commit, git push or any git command that changes anything. When you ask for a commit, the agent makes it under your permission rules.
  • No prompt other than the one above, and no slash command other than /rename, /clear and /compact.

Uninstall

/plugin uninstall autodrive-handoff@ddaanet

Further reading

License

MIT

Source 1 files
hooks/handoff.ts 84 lines
1// handoff mod: runs the transition the checkpoint armed.
2//
3// classic.Stop only records the turn's cwd and session id (Esc fires no Stop,
4// so an aborted turn arms nothing). The drive runs inline from the turn.complete that follows:
5// the host refuses $.command.run from a Stop hook and from any timer started
6// there. The sentinel is never read here; scripts/drive-take.sh takes it and
7// prints the plan, so the one parser (handoff_drive_read) stays the only one.
8// The session id goes with the cwd: drive-take.sh takes only an armed file this
9// session owns, since every session sharing the root ends turns there.
10
11let stoppedIn: string | null = null
12let stoppedSession = ''
13
14export function register(on: any) {
15  on('classic.Stop', async ($: any, e: any, next: any) => {
16    stoppedIn = e.cwd
17    stoppedSession = e.session_id ?? ''
18    return next(e)
19  })
20
21  on('turn.complete', async ($: any, e: any, next: any) => {
22    const cwd = stoppedIn
23    const session = stoppedSession
24    stoppedIn = null
25    stoppedSession = ''
26    if (cwd !== null) await drive($, cwd, session)
27    return next(e)
28  })
29}
30
31async function drive($: any, cwd: string, session: string) {
32  const sentinel = `${cwd}/.claude/autodrive`
33  // A worktree or subdir cwd falls through: the script owns root resolution.
34  if (cwd === (await $.session.root()) && !(await $.fs.exists(sentinel))) return
35
36  const out = await $.process.run(['bash', `${$.plugin.root}/scripts/drive-take.sh`, cwd, session])
37  if (out.stdout.trim() === '') return
38  const plan = JSON.parse(out.stdout)
39  $.ui.toast(plan.message)
40  // A Stop with no session id: drive-take.sh took nothing and said so. Recorded
41  // like a rejected line, so the next prompt reports it rather than it passing
42  // as a toast alone.
43  if (session === '' && plan.failFile) {
44    await recordFailure($, plan.failFile, plan.message)
45    return
46  }
47
48  for (const line of [...plan.before, ...plan.after]) {
49    // Parity with the old composer check: never type over a person.
50    if ((await $.prompt.read()).text !== '') return
51    try {
52      await runLine($, line)
53    } catch (err: any) {
54      const reason = `handoff: ${line} failed: ${err?.message ?? String(err)}`
55      if (await recordFailure($, plan.failFile, reason)) $.ui.toast(reason)
56      return
57    }
58  }
59}
60
61// Writes the failure record for the next prompt's report. $.fs.write follows a
62// link and has no replace-not-follow form, so a link at the name, dangling or
63// not, is checked for first (one stat; a missing file rejects it) and not
64// written through: the toast then says the record was skipped. A link put
65// there between the stat and the write is still followed — the bound, in a
66// project directory the user trusts. Returns false when it toasted instead.
67async function recordFailure($: any, failFile: string, text: string): Promise<boolean> {
68  const st = await $.fs.stat(failFile).catch(() => undefined)
69  if (st?.isLink) {
70    $.ui.toast(`${text} (not recorded: ${failFile} is a symbolic link)`)
71    return false
72  }
73  await $.fs.write(failFile, `${text}\n`)
74  return true
75}
76
77async function runLine($: any, line: string) {
78  if (!line.startsWith('/')) return $.prompt.submit({ text: line, asUser: true })
79  const space = line.indexOf(' ')
80  const command = space < 0 ? line.slice(1) : line.slice(1, space)
81  const args = space < 0 ? '' : line.slice(space + 1)
82  return $.command.run({ command, args })
83}
84