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

git clone https://github.com/joshhornby/dotfiles.git ~/dotfiles
cd ~/dotfiles
./scripts/init
This installs Homebrew and dependencies, then symlinks everything via [stow][].
If you prefer to set things up manually:
brew install stow gh
cd ~/dotfiles
make reload
| Command | Description |
|---|---|
agent setup | Create git worktrees and iTerm tabs for parallel Claude Code instances |
agent status | Show progress of each agent |
agent cleanup | Remove worktrees and branches |
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.
.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.
Type /<name> to run one.
| Command | When to use |
|---|---|
/outcome | Give an engineering outcome to an agent team and let it run to completion, with a watchdog and an evidence gate |
/commit-and-push | Commit all changes with an auto-generated conventional message, then push |
/ship-and-watch | Commit, push, open a PR, then watch CI and review comments — never auto-merges |
/resolve-pr-comments | Pull PR and Copilot review comments, triage them, apply the chosen fixes as a fresh commit, then resolve the threads |
/save-progress | Snapshot the current session's context so it isn't lost |
/recap | Restore context from a previously saved progress snapshot |
/compound | Roll session learnings into persistent, reusable knowledge |
Claude loads these automatically when a task matches; invoke one directly with /<name>.
| Skill | When to use |
|---|---|
blueprint | Build a feature from scratch with a spec and verification gates |
harness | Add scaffolding (CLAUDE.md, docs, invariants) to make a repo AI-ready |
to-prd | Turn the current conversation into a PRD on the issue tracker |
to-issues | Break a plan or PRD into independently-grabbable issues |
grill-me | Stress-test a plan or design by being interviewed until it holds up |
prototype | Throw together a quick prototype to sanity-check a design before committing |
typescript-strict | Model state as discriminated unions, then hold it with branded types, boundary schemas and strict flags |
technical-writing | Write developer-facing docs — READMEs, guides, proposals, PR descriptions |
josh-writing-voice | Apply my personal voice to prose I publish, then run the humanizer pass |
humanizer | Strip AI tells from any text to make it read as human-written |
diagrams | Create diagrams and visualisations (Mermaid, Graphviz, and more) |
expectations | Capture learnings, gotchas, and decisions into the right docs while fresh |
claude-doctor | Analyse recent learnings and open a PR to improve this Claude setup |
testing | Write behaviour-driven tests — factories, file structure, what to assert |
mutation-testing | Prove 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
hooks/calm.tsx 125 lines1import 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