SLOPSHOPPER

calm

Hide tool noise and show one live status line while Claude works. Needs CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1.

newspinnerrowsguardcommandstatus
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · calm
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ✓ Edited auth.ts ⏺ 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 › /calm ⎿ calm: Mode: normal. Use /calm off, /calm normal or /calm zen. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Tool row
✓ Edited auth.ts
README

Josh's Dotfiles

Quick Start

git clone https://github.com/joshhornby/dotfiles.git ~/dotfiles
cd ~/dotfiles
./scripts/init

This installs Homebrew and dependencies, then symlinks everything via [stow][].

Manual Setup

If you prefer to set things up manually:

brew install stow gh
cd ~/dotfiles
make reload

Scripts

CommandDescription
agent setupCreate git worktrees and iTerm tabs for parallel Claude Code instances
agent statusShow progress of each agent
agent cleanupRemove worktrees and branches

Claude Code

Personal Claude Code config lives under .claude/ and is symlinked into ~/.claude by stow. Team skills come from the separate journey-engineering plugin.

.claude/CLAUDE.md holds the global working rules that apply to every session — no any, object params for functions with 2+ arguments, no gratuitous comments, British spelling, pinned dependency versions, no-attribution commits, and behaviour-over-mocks testing. Project-specific conventions belong in that project's own CLAUDE.md, not here.

.claude/hooks/house-rules.py runs on PreToolUse and denies the call when the text about to be written reads as AI output. It reads markdown, code comments, git commit and gh message text, and MCP text fields. It also blocks the any type in new TypeScript. Mark a line // any: <reason> when no real type exists.

.claude/hooks/team-guard.py runs on the agent-team events. It blocks a task completion that carries no evidence, stops a teammate going quiet while work is still on the board, and writes a run log to ~/.claude/team-guard/run-log.jsonl. Every check fails open, so a run never stalls because the guard could not read its state.

Agents

.claude/agents/ holds the reusable roles for the multi-agent engineering organisation: shadow-lead, project-lead, implementer, tester, researcher and reviewer. The shared protocol lives in the "Engineering organisation" section of .claude/CLAUDE.md. Read .claude/agents/README.md for the role table and the Agent Teams requirement.

Commands

Type /<name> to run one.

CommandWhen to use
/outcomeGive an engineering outcome to an agent team and let it run to completion, with a watchdog and an evidence gate
/commit-and-pushCommit all changes with an auto-generated conventional message, then push
/ship-and-watchCommit, push, open a PR, then watch CI and review comments — never auto-merges
/resolve-pr-commentsPull PR and Copilot review comments, triage them, apply the chosen fixes as a fresh commit, then resolve the threads
/save-progressSnapshot the current session's context so it isn't lost
/recapRestore context from a previously saved progress snapshot
/compoundRoll session learnings into persistent, reusable knowledge
Skills

Claude loads these automatically when a task matches; invoke one directly with /<name>.

SkillWhen to use
blueprintBuild a feature from scratch with a spec and verification gates
harnessAdd scaffolding (CLAUDE.md, docs, invariants) to make a repo AI-ready
to-prdTurn the current conversation into a PRD on the issue tracker
to-issuesBreak a plan or PRD into independently-grabbable issues
grill-meStress-test a plan or design by being interviewed until it holds up
prototypeThrow together a quick prototype to sanity-check a design before committing
typescript-strictModel state as discriminated unions, then hold it with branded types, boundary schemas and strict flags
technical-writingWrite developer-facing docs — READMEs, guides, proposals, PR descriptions
josh-writing-voiceApply my personal voice to prose I publish, then run the humanizer pass
humanizerStrip AI tells from any text to make it read as human-written
diagramsCreate diagrams and visualisations (Mermaid, Graphviz, and more)
expectationsCapture learnings, gotchas, and decisions into the right docs while fresh
claude-doctorAnalyse recent learnings and open a PR to improve this Claude setup
testingWrite behaviour-driven tests — factories, file structure, what to assert
mutation-testingProve the tests catch bugs with Stryker before a PR goes up

testing and mutation-testing come from [Paul Hammond's dotfiles][citypaul] under the MIT licence. Each folder holds a LICENSE file with the attribution.

[stow]: https://www.gnu.org/software/stow/ [citypaul]: https://github.com/citypaul/.dotfiles

Source 1 files
hooks/calm.tsx 125 lines
1import type { Register } from "claude-code";
2
3type Mode = "off" | "normal" | "zen";
4
5const modes: readonly Mode[] = ["off", "normal", "zen"];
6
7const isMode = (value: unknown): value is Mode =>
8  typeof value === "string" && (modes as readonly string[]).includes(value);
9
10const field = ({ input, key }: { input: unknown; key: string }): string | undefined => {
11  if (typeof input !== "object" || input === null) return undefined;
12  const value: unknown = Reflect.get(input, key);
13  return typeof value === "string" && value.length > 0 ? value : undefined;
14};
15
16const basename = (path: string): string => path.split("/").filter(Boolean).pop() ?? path;
17
18const shorten = (text: string): string => {
19  const line = text.split("\n")[0] ?? "";
20  return line.length > 60 ? `${line.slice(0, 57)}...` : line;
21};
22
23type Verb = { present: string; past: string };
24
25const describe = ({ tool, input }: { tool: string; input: unknown }): Verb => {
26  const path = field({ input, key: "file_path" }) ?? field({ input, key: "notebook_path" });
27  const target = path ? ` ${basename(path)}` : "";
28  switch (tool) {
29    case "Read":
30      return { present: `Reading${target}`, past: `Read${target}` };
31    case "Edit":
32    case "MultiEdit":
33    case "NotebookEdit":
34      return { present: `Editing${target}`, past: `Edited${target}` };
35    case "Write":
36      return { present: `Writing${target}`, past: `Wrote${target}` };
37    case "Bash": {
38      const what = shorten(field({ input, key: "description" }) ?? field({ input, key: "command" }) ?? "command");
39      return { present: `Running ${what}`, past: `Ran ${what}` };
40    }
41    case "Grep":
42    case "Glob":
43      return { present: "Searching the codebase", past: "Searched the codebase" };
44    case "Agent":
45    case "Task":
46      return { present: "Delegating to an agent", past: "Delegated to an agent" };
47    case "WebFetch":
48    case "WebSearch":
49      return { present: "Researching the web", past: "Researched the web" };
50    default:
51      return { present: `Using ${tool}`, past: `Used ${tool}` };
52  }
53};
54
55export const register: Register = (on) => {
56  let mode: Mode = "normal";
57
58  on("session.start", async ($, e, next) => {
59    const stored = await $.store.get("mode");
60    if (isMode(stored)) mode = stored;
61    await $.command.register({
62      name: "calm",
63      description: "Set how much tool activity Claude Code shows",
64      argumentHint: "[off|normal|zen]",
65      immediate: true,
66    });
67    return next(e);
68  });
69
70  on("command.run", { command: "calm" }, async ($, e) => {
71    const requested = e.args.trim();
72    if (requested === "") return { text: `Mode: ${mode}. Use /calm off, /calm normal or /calm zen.` };
73    if (!isMode(requested)) return { text: `Unknown mode "${requested}". Use off, normal or zen.` };
74    mode = requested;
75    await $.store.set("mode", mode);
76    if (mode === "off") $.ui.status(undefined);
77    // Render answers are cached per input, so rows already on screen keep the old mode until invalidated.
78    $.ui.invalidate("ui.render");
79    return { text: `Mode: ${mode}.` };
80  });
81
82  on("tool.call", async ($, e, next) => {
83    // A subagent's calls would make the status line flicker between two streams of work.
84    if (mode === "off" || e.agentId !== undefined) return next(e);
85    $.ui.status(describe({ tool: e.tool, input: e }).present);
86    return next(e);
87  });
88
89  on("turn.complete", ($, e, next) => {
90    $.ui.status(undefined);
91    return next(e);
92  });
93
94  on("ui.render", { component: "ToolUse" }, ($, e, next) => {
95    const { tool, input, isRunning, isErrored, isInterrupted } = e.props;
96    if (mode === "off" || isErrored || isInterrupted) return next(e);
97    const { Box, Text } = $.ui.resolve(e);
98    if (mode === "zen" || isRunning) return <Box />;
99    return (
100      <Box>
101        <Text color="green">✓ </Text>
102        <Text dimColor>{describe({ tool, input }).past}</Text>
103      </Box>
104    );
105  });
106
107  on("ui.render", { component: "ToolResult" }, ($, e, next) => {
108    if (mode === "off" || e.props.isErrored) return next(e);
109    const { Box } = $.ui.resolve(e);
110    return <Box />;
111  });
112
113  on("ui.render", { component: "ToolGroup" }, ($, e, next) => {
114    if (mode !== "zen" || e.props.calls.some((call) => call.isErrored)) return next(e);
115    const { Box } = $.ui.resolve(e);
116    return <Box />;
117  });
118
119  on("ui.render", { component: "TurnDuration" }, ($, e, next) => {
120    if (mode !== "zen") return next(e);
121    const { Box } = $.ui.resolve(e);
122    return <Box />;
123  });
124};
125