SLOPSHOPPER

auto-context

Adds git branch, dirty count, last commit and next TODOS to the session's system prompt

newcommandpromptprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · auto-context
› 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 › /auto-context ⎿ auto-context: Session context (auto-context, as of the last turn): ⎿ auto-context: Git branch: ⎿ auto-context: Uncommitted files: 2 ⎿ auto-context: Last commit: a1b2c3d fix: refresh expired tokens (2 minutes ago) ⎿ auto-context: 9f8e7d6 feat: add audit log (3 hours ago) ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

auto-context

Gives Claude a short, always-fresh picture of the repository state without having to ask for it.

What it does

  • Adds a session section to the system prompt:
  • git branch, with ahead/behind counts when non-zero
  • number of uncommitted files
  • last commit subject
  • up to 5 open (- [ ]) items of TODOS.md: those under a ## Next heading, otherwise the first open items anywhere in the file
  • Computed at session start and refreshed after each main turn (cached between turns).
  • Nothing is injected outside a git repository without a TODOS.md containing open items.

Commands

CommandEffect
/auto-contextShows the text currently injected
/auto-context offStops injecting (kept across sessions)
/auto-context onResumes injecting and refreshes

Install

claude --plugin-dir /path/to/ModsTools/mods/auto-context

Limits

  • The section reflects the state as of the last completed turn, not live changes made since.
  • Git and TODOS.md are read from the session folder only.

Develop

claude plugin validate mods/auto-context
claude plugin test mods/auto-context   # 9 tests
Source 3 files
hooks/register.ts 76 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { formatContext, nextTodos, parseStatus } from './context'
5import type { GitInfo } from './context'
6
7const SECTION = 'auto-context:session'
8const text = atom({ plugin: 'auto-context', key: 'text' } as const, null)
9
10async function isEnabled($: EngineInterface): Promise<boolean> {
11  return (await $.store.get('enabled')) !== false
12}
13
14async function git($: EngineInterface): Promise<GitInfo | null> {
15  try {
16    const status = await $.process.run(['git', 'status', '--porcelain=v2', '--branch'], { cwd: await $.session.cwd() })
17    if (status.exitCode !== 0) return null
18    const log = await $.process.run(['git', 'log', '-1', '--format=%s'], { cwd: await $.session.cwd() })
19
20    return { ...parseStatus(status.stdout), subject: log.exitCode === 0 ? log.stdout.trim() : '' }
21  } catch {
22    return null
23  }
24}
25
26async function todos($: EngineInterface): Promise<string[]> {
27  try {
28    return nextTodos(await $.fs.read(`${await $.session.cwd()}/TODOS.md`))
29  } catch {
30    return []
31  }
32}
33
34async function refresh($: EngineInterface) {
35  const value = formatContext(await git($), await todos($))
36  await update($, text, () => value)
37  $.ui.invalidate('prompt.section')
38}
39
40export const register: Register = on => {
41  on('session.start', async ($, e, next) => {
42    await $.command.register({ name: 'auto-context', description: 'Show the injected session context; "off" or "on" toggles it' })
43    await refresh($)
44
45    return next(e)
46  })
47
48  on('turn.complete', async ($, e, next) => {
49    if (e.agentId === undefined) await refresh($)
50
51    return next(e)
52  })
53
54  on('prompt.compose', async ($, e, next) => {
55    const composed = await next(e)
56    const value = await read($, text)
57    if (value === null || !(await isEnabled($))) return composed
58
59    return { sections: [...composed.sections, { id: SECTION, text: value, scope: 'session' as const }] }
60  })
61
62  on('command.run', { command: 'auto-context' }, async ($, e) => {
63    const arg = e.args.trim()
64    if (arg === 'on' || arg === 'off') {
65      await $.store.set('enabled', arg === 'on')
66      $.ui.invalidate('prompt.section')
67      if (arg === 'on') await refresh($)
68
69      return { text: `auto-context ${arg}.` }
70    }
71    if (!(await isEnabled($))) return { text: 'auto-context is off.' }
72
73    return { text: (await read($, text)) ?? 'Nothing to inject (no git repository, no TODOS.md items).' }
74  })
75}
76
hooks/context.ts 56 lines
1export type GitInfo = { branch: string; ahead: number; behind: number; dirty: number; subject: string }
2
3export const MAX_TODOS = 5
4
5// Parses `git status --porcelain=v2 --branch` output.
6export function parseStatus(out: string): Omit<GitInfo, 'subject'> {
7  let branch = ''
8  let ahead = 0
9  let behind = 0
10  let dirty = 0
11  for (const line of out.split('\n')) {
12    if (line.startsWith('# branch.head ')) branch = line.slice(14).trim()
13    else if (line.startsWith('# branch.ab ')) {
14      const m = /\+(\d+) -(\d+)/.exec(line)
15      if (m) {
16        ahead = Number(m[1])
17        behind = Number(m[2])
18      }
19    } else if (line !== '' && !line.startsWith('#')) dirty++
20  }
21  return { branch, ahead, behind, dirty }
22}
23
24// First 5 unchecked items under a `## Next` heading, else the first 5 anywhere.
25export function nextTodos(text: string): string[] {
26  const all: string[] = []
27  const next: string[] = []
28  let inNext = false
29  for (const line of text.split('\n')) {
30    const heading = /^#{1,6}\s+(.*)$/.exec(line)
31    if (heading) inNext = /^next\b/i.test(heading[1]?.trim() ?? '')
32    const item = /^\s*[-*]\s+\[ \]\s+(.*\S)\s*$/.exec(line)
33    if (item?.[1]) {
34      all.push(item[1])
35      if (inNext) next.push(item[1])
36    }
37  }
38  return (next.length > 0 ? next : all).slice(0, MAX_TODOS)
39}
40
41export function formatContext(git: GitInfo | null, todos: readonly string[]): string | null {
42  const lines: string[] = []
43  if (git) {
44    const sync = [git.ahead > 0 ? `ahead ${git.ahead}` : '', git.behind > 0 ? `behind ${git.behind}` : '']
45      .filter(Boolean)
46      .join(', ')
47    lines.push(`Git branch: ${git.branch}${sync ? ` (${sync})` : ''}`)
48    lines.push(`Uncommitted files: ${git.dirty}`)
49    if (git.subject) lines.push(`Last commit: ${git.subject}`)
50  }
51  if (todos.length > 0) lines.push('Next TODOS:', ...todos.map(t => `- [ ] ${t}`))
52  if (lines.length === 0) return null
53
54  return ['Session context (auto-context, as of the last turn):', ...lines].join('\n')
55}
56
types/index.d.ts 8 lines
1export type ContextText = string | null
2
3declare module 'claude-code' {
4  interface PluginState {
5    'auto-context': { text: ContextText }
6  }
7}
8