SLOPSHOPPER

ship-guard

Shows when the ship skill is not loaded during an open /ship cycle and forces the model to load it.

newbandtoastpromptprocesstimer
v0.1.0no licenseupdated 2026-10-22queirozlc/claude-config/.claude/skills/ship-guard
A shopper browsing a rack in a slop shop
README

Claude Code user config

Portable Claude Code setup. The git root is $HOME. Only ~/.claude and ~/.agents are tracked, through a whitelist in ~/.gitignore.

This file is not loaded into Claude sessions. Only CLAUDE.md, rules/, skills/, agents/, output-styles/ and the hooks in settings.json are.

Install on a new machine

Using an agent: paste ~/PROMPT.md into it. By hand:

cd ~
git init -b main
git remote add origin <repo-url>
git fetch origin
git reset --hard origin/main   # writes only the tracked files; existing untracked files stay
sh ~/.claude/install.sh

Tracked

PathWhat
.claude/CLAUDE.mdGlobal instructions
.claude/settings.jsonPermissions, hooks, env, enabled plugins, status line
.claude/statusline.shStatus line (needs git, jq)
.claude/rules/Always-on rules
.claude/skills/Skills. Most are symlinks into ~/.agents/skills
.claude/agents/Custom subagents
.claude/output-styles/Output styles
.claude/hooks/Hook scripts referenced from settings.json. no-bash-file-edits.sh blocks file edits made through Bash so the agent uses Read, Edit and Write
.agents/skills/Skill content shared by all agents (Claude Code, Codex, Cursor)
.agents/ship/ledger-stop.pyStop hook for the ship workflow
.agents/ship/state/Only .gitkeep. Ship state is never tracked

Everything else under $HOME is ignored. Sessions, projects, memories, history, caches, credentials and backups must stay untracked.

Dependencies

Installed by install.sh:

ToolUsed bySource
codegraphMCP server codegraph, UserPromptSubmit hook codegraph prompt-hookhttps://github.com/colbymchenry/codegraph
ai-memoryMCP server ai-memory (http://127.0.0.1:49374), hooks on most lifecycle eventshttps://github.com/akitaonrails/ai-memory
ponytailPlugin ponytail@ponytailhttps://github.com/DietrichGebert/ponytail
paper-desktopPlugin paper-desktop@paperhttps://github.com/paper-design/agent-plugins
atlassian, typescript-lspPlugins from claude-plugins-officialhttps://github.com/anthropics/claude-plugins-official
maestriThe maestri-* skills. install.sh exports MAESTRI_CLI and adds the CLI to PATH in your shell rc when /Applications/Maestri.app exists (macOS only)Maestri desktop app
context7, mobbinHTTP MCP servers (stored in ~/.claude.json, not in this repo)https://mcp.context7.com/mcp, https://api.mobbin.com/mcp

Notes

  • ai-memory install-hooks rewrites the ai-memory hook commands in settings.json with absolute paths for the current machine. Commit that diff after running it on a new machine.
  • .claude/skills/elixir is a link into a local project and is gitignored.
  • ~/.agents/.skill-lock.json records where each skill in ~/.agents/skills came from, for npx skills update.
Source 3 files
hooks/register.tsx 111 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Scan } from '../types'
5import { NONE, failed, idleLine, parseScan } from './logic'
6
7const isLoaded = atom({ plugin: 'ship-guard', key: 'isLoaded' } as const, false)
8const tried = atom({ plugin: 'ship-guard', key: 'tried' } as const, false)
9const last = atom({ plugin: 'ship-guard', key: 'scan' } as const, NONE as Scan)
10
11// ~/.agents/ship/ledger.py is the one reader of ledgers. The Stop hooks and context-handoff use it too.
12async function scan($: any): Promise<Scan> {
13  try {
14    if (!(await $.env.get('MAESTRI_TERMINAL_ID'))) return NONE
15    const reader = `${await $.env.get('HOME')}/.agents/ship/ledger.py`
16    return parseScan(await $.process.run(['python3', reader, await $.session.cwd()]))
17  } catch (err) {
18    return failed((err as Error).message)
19  }
20}
21
22async function refresh($: any) {
23  const found = await scan($)
24  await update($, last, () => found)
25  return found
26}
27
28// Runs /ship once per context. If the command fails, the prompt note still forces the load.
29async function autoLoad($: any) {
30  const found = await refresh($)
31  if (!found.ledgers.length || (await read($, isLoaded)) || (await read($, tried))) return
32  await update($, tried, () => true)
33  $.command
34    .run({ command: 'ship', args: 'resume the open cycle from its ledger. If `maestri list` shows a note named "Handoff from ...", read it too.' })
35    .catch(() => $.ui.toast('ship-guard: could not run /ship. Type /ship.'))
36}
37
38export const register: Register = on => {
39  on('session.start', async ($, e, next) => {
40    $.clock.after(1500, () => {
41      autoLoad($)
42    })
43    return next(e)
44  })
45
46  // /clear fires no session.start: reset here, the context is empty again.
47  on('session.end', async ($, e, next) => {
48    if (e.reason === 'clear') {
49      await update($, isLoaded, () => false)
50      await update($, tried, () => false)
51      $.clock.after(1500, () => {
52        autoLoad($)
53      })
54    }
55    return next(e)
56  })
57
58  // Typed /ship or loaded through the Skill tool: same event.
59  on('skill.prompt', { skill: 'ship' }, async ($, e, next) => {
60    await update($, isLoaded, () => true)
61    return next(e)
62  })
63
64  // Force: until the skill is loaded, every prompt carries the order to load it.
65  on('prompt.submit', async ($, e, next) => {
66    if (await read($, isLoaded)) return next(e)
67    const found = (await refresh($)).ledgers
68    if (!found.length || /^\s*\/ship\b/.test(e.text)) return next(e)
69    const list = found.map(l => `- ${l.key} (Phase: ${l.phase}) Next: ${l.next}`).join('\n')
70    return next({
71      ...e,
72      context: [
73        ...(e.context ?? []),
74        `ship-guard: open /ship cycle ledger(s) exist for this repo and the ship skill is NOT loaded in this context.\n${list}\nBefore anything else, load the ship skill with the Skill tool and follow its resume rule. Then answer the user's message.`,
75      ],
76    })
77  })
78
79  // A turn can open, close or break a ledger: the band follows.
80  on('turn.complete', async ($, e, next) => {
81    const out = await next(e)
82    await refresh($)
83    return out
84  })
85
86  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
87    const s = await read($, last)
88    if (e.props.hasSurvey || s.seat || (!s.project && !s.problems.length)) return next(e)
89    const { Box, Text } = $.ui.resolve(e)
90    const keys = s.ledgers.map(l => l.key).join(', ')
91    const loaded = await read($, isLoaded)
92    // The band has one tree: carry the plugins beneath (context-handoff's ctx line) inside it.
93    const below = await next(e)
94    return (
95      <Box flexDirection="column">
96        {s.problems.map(p => (
97          <Text color="red">ship: {p}</Text>
98        ))}
99        {!keys ? (
100          <Text dimColor>{idleLine(s)}</Text>
101        ) : loaded ? (
102          <Text dimColor>ship: loaded ({keys})</Text>
103        ) : (
104          <Text color="red">ship: NOT loaded, open cycle {keys}. Loading it now.</Text>
105        )}
106        {below}
107      </Box>
108    )
109  })
110}
111
hooks/logic.ts 16 lines
1import type { Scan } from '../types'
2
3export const NONE: Scan = { project: null, seat: false, floor: null, branch: '', ledgers: [], problems: [] }
4
5// A failure is a problem to show, never an empty answer: an empty answer looks like "no open cycle".
6export const failed = (why: string): Scan => ({ ...NONE, problems: [`ledger.py did not run: ${why}`] })
7
8export function parseScan(run: { exitCode: number; stdout: string; stderr: string }): Scan {
9  if (run.exitCode !== 0) return failed(run.stderr.trim().split('\n').pop() || `exit ${run.exitCode}`)
10  return JSON.parse(run.stdout)
11}
12
13// The line for a Maestro with no open ledger: it shows that the mod works and found none.
14export const idleLine = (s: Scan) =>
15  s.floor ? `ship: no open ledger names this floor (folder ${s.floor}, branch ${s.branch || 'none'})` : 'ship: no open cycle'
16
types/index.d.ts 10 lines
1// What ~/.agents/ship/ledger.py prints: the open ledgers of the agent in a folder, and every problem.
2export type OpenLedger = { key: string; path: string; phase: string; next: string; master: string; routed: boolean; mtimeMs: number }
3export type Scan = { project: string | null; seat: boolean; floor: string | null; branch: string; ledgers: OpenLedger[]; problems: string[] }
4
5declare module 'claude-code' {
6  interface PluginState {
7    'ship-guard': { isLoaded: boolean; tried: boolean; scan: Scan }
8  }
9}
10