Typeahead for recent handoff plan files when you type handoff

A Claude Code mod that autocompletes handoff plan files in the prompt box. Type handoff after /implement (or anywhere in a prompt) and pick a recent handoff file from the typeahead; it is replaced with the file's absolute path. Keep typing to filter by file name (handoff-learn).
At the prompt of a Claude Code terminal session:
/plugin install handoff-autocomplete --marketplace gugahoi/claude-mods
Answer y to add the marketplace, then pick a scope.
*handoff*.md files in the OS temp directory ($TMPDIR, else /tmp), where the /handoff skill saves them.handoff in the typed word must appear in the file name (case-insensitive): handoff-learn matches handoff-learn-pull-to-refresh.md.claude plugin validate .
claude plugin test .
claude --plugin-dir .hooks/register.ts 46 lines1import type { EngineInterface, PromptAutocompleteSuggestion, Register } from 'claude-code'
2
3let tmpdir: string | undefined // TMPDIR never changes for the life of the process
4
5export const ago = (ms: number) => {
6 const m = Math.floor(ms / 60_000)
7 if (m < 1) return 'just now'
8 if (m < 60) return `${m}m ago`
9 return m < 1440 ? `${Math.floor(m / 60)}h ago` : `${Math.floor(m / 1440)}d ago`
10}
11
12// First markdown heading, else the first non-empty line.
13export const heading = (text: string) => {
14 const lines = text.split('\n').map(l => l.trim()).filter(Boolean)
15 return (lines.find(l => l.startsWith('#')) ?? lines[0] ?? '').replace(/^#+\s*/, '')
16}
17
18// `query` is what follows `handoff` in the token, matched anywhere in the file name.
19export async function handoffs($: EngineInterface, query: string): Promise<PromptAutocompleteSuggestion[]> {
20 tmpdir ??= ((await $.env.get('TMPDIR')) ?? '/tmp').replace(/\/+$/, '')
21 const dir = tmpdir
22 const q = query.toLowerCase()
23 const files = (await $.fs.list(dir))
24 .filter(f => {
25 const name = f.name.toLowerCase()
26 return f.kind === 'file' && name.endsWith('.md') && name.includes('handoff') && name.includes(q)
27 })
28 .sort((a, b) => b.mtimeMs - a.mtimeMs)
29 .slice(0, 10)
30 return Promise.all(
31 files.map(async f => {
32 const text = `${dir}/${f.name}`
33 const title = heading(await $.fs.read(text).catch(() => ''))
34 const age = ago(Date.now() - f.mtimeMs)
35 return { text, label: f.name, description: title ? `${age} · ${title}` : age }
36 }),
37 )
38}
39
40export const register: Register = on => {
41 on('prompt.autocomplete', { token: /^handoff/i }, async ($, e, next) => {
42 const [mine, below] = await Promise.all([handoffs($, e.token.slice('handoff'.length)).catch(() => []), next(e)])
43 return mine.length ? { suggestions: [...below.suggestions, ...mine] } : below
44 })
45}
46