SLOPSHOPPER

compact-handoff

Writes a handoff before each compaction and carries it into the compacted conversation.

newmodel
A shopper browsing a rack in a slop shop
README

dotfiles

GNU Stow-based dotfiles for macOS and Linux. One stow package, home/, mirrors $HOME.

Quickstart

git clone --recursive https://github.com/sustinbebustin/dotfiles.git ~/.dotfiles
~/.dotfiles/dot init

dot init installs Homebrew packages, symlinks configs, and sets zsh as the default shell. Open a new terminal when it finishes.

How it works

Everything under home/ maps 1:1 to $HOME -- home/.config/zsh/ becomes ~/.config/zsh/. Stow is run with --no-folding so each file gets its own symlink rather than the directory being linked wholesale.

Claude Code skills are the exception. The tree at home/.claude/skills/ is skipped by stow and published to ~/.claude/skills by dot stow itself, one folder symlink per skill. Skills may be grouped under a one-level category dir for clarity; that layer is flattened away when linking, since Claude Code only reads ~/.claude/skills/<name>/SKILL.md.

A category dir holding .claude-plugin/plugin.json doubles as a Claude Code plugin, listed in the root .claude-plugin/marketplace.json. dot ignores the hidden manifest dir, so local linking is unchanged. The meta skills install without cloning this repo:

claude plugin marketplace add sustinbebustin/.dotfiles --sparse .claude-plugin
claude plugin install meta@sustinbebustin

Claude Code mods in home/.claude/mods/ are published the same way, one folder symlink per mod into ~/.claude/mods, because the plugin loader rejects per-file links. settings.base.json points CLAUDE_CODE_PLUGIN_DIRS at that folder, so every mod there loads in every session.

The Go hooks in home/.claude/hooks/ (PreToolUse guards, PostToolUse format/lint) are stowed as built *-bin binaries only -- dot stow builds them first, since they are per-platform and gitignored -- and registered in home/.claude/settings.base.json, which dot stow merges into the gitignored home/.claude/settings.json.

Structure

~/.dotfiles/
├── dot                              # CLI (v4.0.0)
├── packages/
│   └── Brewfile                     # Homebrew packages
├── home/                            # the stow package -> $HOME
│   ├── .zshenv
│   ├── .claude/                     # Claude Code config
│   │   ├── skills/                  # agent skills (published by dot, not stow)
│   │   └── hooks/                   # Go guard hooks (sources ignored, *-bin stowed)
│   └── .config/
│       ├── zsh/                     # shell (ZDOTDIR)
│       ├── starship.toml            # prompt
│       ├── git/                     # git config + aliases
│       ├── ghostty/                 # terminal
│       ├── tmux/                    # tmux + TPM (submodule)
│       ├── ripgrep/                 # rg defaults
│       └── karabiner/               # key remapping (macOS)
└── docs/

Commands

dot init              Full bootstrap (packages, stow, shell)
dot stow              Re-stow dotfiles
dot update            Pull, update packages, re-stow
dot doctor            Health check with remediation hints
dot package add       Install and track in Brewfile
dot package remove    Remove from package list
dot package list      Show packages with install status
dot claude add        Add a Claude account (~/.claude-<name>, run as <name>)
dot claude remove     Unlink a Claude account, keeping its login
dot claude list       Show Claude accounts
dot retry-failed      Retry failed package installations
dot link              Symlink dot into ~/.local/bin
dot edit              Open dotfiles in $EDITOR
dot gen-ssh-key       Generate ed25519 SSH key
dot completions       Generate zsh completions

Documentation

Source 1 files
hooks/register.ts 75 lines
1import type { ModelForkResult, Register, SessionMessage } from 'claude-code'
2
3const HANDOFF_PROMPT = `The conversation is about to be compacted: it will be replaced with a summary, and you keep working in this same session afterwards. Early instructions, tool output, and reasoning you have not written down will be gone.
4
5Write the handoff that carries this work across. The reader is you, right after compaction. Record what you would be stuck without:
6- the user's decisions and constraints, as they stated them
7- what you have established and ruled out, with its evidence (path:line, command output)
8- the exact state of in-flight work: what is done, what is half-done, which files changed
9- the next concrete action
10
11Reference specs, plans, issues, commits, and diffs by path or URL; their content stays where it is. Redact secrets and credentials. Keep it under 5,000 tokens.
12
13Reply with the handoff document alone, in Markdown. Your tools are unavailable for this reply.`
14
15const handoffPrompt = (instructions: string | undefined): string =>
16  instructions === undefined || instructions.trim() === ''
17    ? HANDOFF_PROMPT
18    : `${HANDOFF_PROMPT}\n\nAfter compacting, the session will do this: ${instructions.trim()}\nWeight the handoff toward what that work needs.`
19
20type ForkFailure = Extract<ModelForkResult, { isAnswered: false }>
21
22const describeFailure = (failure: ForkFailure): string => {
23  switch (failure.reason) {
24    case 'api-error':
25      return `the API returned ${failure.error} (status ${failure.status ?? 'none'})`
26    case 'empty-reply':
27      return 'the model replied with no text'
28    case 'aborted':
29      return 'the request was interrupted'
30    case 'nothing-to-fork':
31      return 'the conversation has no response yet'
32  }
33}
34
35const handoffMessage = (text: string, path: string | undefined): SessionMessage => ({
36  role: 'user',
37  text: `${path === undefined ? 'Handoff you wrote just before this compaction' : `Handoff you wrote just before this compaction (also saved at ${path})`}. It is your own record of the work; read it with the summary above.\n\n${text}`,
38  toolUses: [],
39})
40
41export const register: Register = on => {
42  on('session.compact', async ($, e, next) => {
43    // A subagent's own compaction, and a precompute whose result is kept for later, carry no handoff.
44    if (e.agentId !== undefined || e.trigger === 'precompute') return next(e)
45
46    const fork = await $.model.fork({ prompt: handoffPrompt(e.instructions) })
47    if (!fork.isAnswered) {
48      const why = describeFailure(fork)
49      // A manual /compact can wait: leave the conversation whole so no work is lost.
50      if (e.trigger === 'manual') {
51        return {
52          skip: `compact-handoff: the handoff was not written because ${why}. The conversation is unchanged. Run /compact again, or disable compact-handoff in /plugin to compact without a handoff.`,
53        }
54      }
55      $.ui.log(`the handoff was not written because ${why}; compacting without one.`)
56      return next(e)
57    }
58
59    const sessionId = await $.session.id()
60    const tmp = (await $.env.get('TMPDIR')) ?? '/tmp'
61    const target = `${tmp.replace(/\/$/, '')}/claude-handoff-${sessionId}.md`
62    const saved = await $.fs.write(target, fork.text).then(
63      () => target,
64      () => undefined,
65    )
66    if (saved === undefined) $.ui.log(`could not save the handoff to ${target}; it is still carried into the conversation.`)
67
68    const compacted = await next(e)
69    if (compacted.skip !== undefined) return compacted
70
71    $.ui.log(saved === undefined ? 'handoff carried across compaction.' : `handoff carried across compaction; saved at ${saved}`)
72    return { ...compacted, messages: [...compacted.messages, handoffMessage(fork.text, saved)] }
73  })
74}
75