Status line with prompt cache minutes left or cold since and context window use

Claude Code is the AI that writes code for you, in a terminal or in the Claude desktop app. It reads its setup from a hidden folder on your computer, ~/.claude. This repository is a public copy of my setup, so you can borrow from it.
Claude Code is good at writing code, but left alone it makes some of the same mistakes again and again. Most of this setup stops those mistakes before they happen. How I work shows how I use it from day to day.
I think this is the next step for free software and open source. Many projects now write their code with AI, but they give back only the source code, never how they used the AI. If a project calls itself open, it should put its money where its mouth is and publish that too. This repository is my part: the rules and hooks I work with are here, and every commit quotes the prompts behind it, collected in PROMPTS.md.
check-not-piped.sh: stops Claude from reporting failed tests as passed, which happens when the test output goes through another command.no-github-posts.sh: stops Claude from posting comments or reviews on GitHub, where they would appear under your name.no-home-search.sh: stops searches of your whole disk, which make the Mac ask you for permission folder after folder.cd-fall-through.sh: stops a command from carrying on in the wrong folder when moving to the right one failed.no-foreground-wait.sh: stops Claude from checking over and over on a task it left running in the background, which keeps it from reading your messages for up to ten minutes.no-slow-binary-grep.sh: stops a search inside a large program file, such as Claude Code itself, that would run for minutes, and shows Claude a way that takes under a second.no-git-checkout.sh: stops Claude from using git checkout, which can overwrite files by mistake, and names the git switch or git restore command instead.no-identity-override.sh: stops Claude from making up a git author, even in a throwaway test repository, and asks you before it changes the name or address your commits carry.calendar-free-busy-only.sh: stops Claude from reading other people's calendar events. It can still see when they are free.slack-user-search-own-workspace.sh: tells Claude that the Slack user search misses members of other companies who write in a shared channel, so it reads that channel instead of saying it cannot see their message.stop-running-stack.sh: makes Claude stop the Docker containers it started before it finishes, so nothing keeps running and eating battery.stop-unreachable-claim.sh: sends Claude back to try the built-in browser, where you are signed in, before it says a page or file is out of reach.stack-held-by-peer.sh: stops one Claude window from stopping Docker containers that another window is using.git-fetch-prune.sh: keeps Claude's picture of GitHub up to date, in the background, so it does not act on old information.git-fetch-before-remote-base.sh: fetches first and waits when Claude is about to branch, rebase or reset from the copy on GitHub, so it never starts from an old one.git-prune-gone-branches.sh: deletes old branches when no work would be lost, and lists the ones it kept.git-submodule-update-after-switch.sh: keeps submodules, repos inside a repo, in step after Claude switches branch.prompt-log.sh: saves every prompt you type, on your computer only, so commits and pull requests can quote the prompts behind them.prompts-md.sh: builds a PROMPTS.md file that lists those prompts per commit, like the one in this repo.prompts-md-current.sh: stops a commit once when Claude did not rebuild that file for it, in a repository that keeps one.no-meta-prompts.sh: keeps prompts about Claude's own setup out of a project's history.abbreviations-listed.sh: holds back a commit whose quoted prompts use an abbreviation an earlier commit spelled out, such as pr for pull request, until the message spells it out too.usage-log.sh: writes down the tokens each turn used, so you can add up a week later.CLAUDE.md: the rules Claude follows in every project. It names no person or company, so you can copy it as it is.claude-md-changed.sh: tells a running session when the rules or the pull request skill were changed in another window.permission-mode-changed.sh: tells a running session when you switched its permission mode, so it never names the old one.prompt-feedback-box.sh: makes sure every reply to a prompt you typed ends with a short note on how your prompt could have been better.memory-plain-characters.sh: points out long dashes in Claude's memory notes, so Claude swaps them for plain punctuation.claude-md-memory-index.sh: when Claude changes a rule in CLAUDE.md, shows it every memory note index, so it fixes the notes that still say the old thing.share-memories.sh: shares Claude's notes about a project with your team. It does nothing until you set it up.settings.json: turns the hooks on, picks my model, blocks a few tools, leaves "Co-Authored-By: Claude" off commits and keeps old sessions for 90 days instead of 30.mods/session-usage: a line under the prompt box that shows how many minutes the cache stays warm (a warm cache makes the next message cheaper) and how full Claude's memory of the chat is. The tokens and what they would cost show once a week instead, in the report of the last /dream run before the weekly reset.presence: my phone gets Claude's pushes, such as a question waiting for an answer, only while the Mac is locked. lock-listener.swift keeps the file that CLAUDE_CLIENT_PRESENCE_FILE in settings.json names in step with the screen lock: it is there while the screen is unlocked, and Claude Code skips the push then. Mac only. This builds it and starts it at every login: ``zsh ~/.claude/presence/install.sh ``skills/pull-requests: how Claude writes a pull request: the description, the steps to test it, screenshots, and how it reviews one.skills/pull-requests/pane-upload.sh: puts screenshots into a pull request from the app's built-in browser, which cannot upload files itself.skills/pull-requests/review-page: turns a pull request's test steps and screenshots into a checklist page you tick off with the keyboard while you review it.skills/slack-draft: Claude drafts a Slack message and puts it on your clipboard with links and bold kept. You paste it and press send. Mac only.skills/dotclaude: how Claude keeps this copy and my own ~/.claude the same, and what it must never publish here. Claude reads it only before it changes the setup, which keeps the rules every session reads shorter.workflows/dream.js: type /dream when you stop for the day. Many agents read your recent sessions and your setup, and come back with suggested changes, each with a ready script. It changes nothing by itself and stops before your usage limits. When ~/.claude/dream/evals-checkout.txt names a copy of this repository, it then reruns the evals below and publishes their numbers.agents/dream-worker.md: a lighter agent for /dream, so it uses fewer tokens.sync.sh: keeps this copy and my own ~/.claude the same, and checks that nothing private gets published. It also stops on a shell script with a syntax error, a file this list leaves out, or a line in CLAUDE.md, the pull request skill or this README wider than 100 characters. --install copies everything onto a new computer.reflow.sh: rewraps the paragraphs an edit changed in CLAUDE.md, the pull request skill or this README at 100 characters, in this copy and in ~/.claude.test/dream-sim: checks how /dream spends your usage. Not installed.test/hooks: feeds every hook the input Claude Code sends it, in a throwaway home folder, and checks what it lets through. Not installed.test/rules: counts how often Claude kept the rules in your own sessions of the last week. It prints counts only, never what was said.test/evals.mjs: runs all of these and fills in the Evals section below.For the full details, open the file: every hook explains itself at the top, and so do sync.sh and dream.js.
This is how I use this setup from day to day, on a MacBook Air with an M5 chip. It leaves out what I work on.
Each morning, before I start anything, I run gm in a terminal:
gm
It is an alias in ~/.zshrc:
alias gm="$HOME/bin/start-of-day-update-and-restart.sh"
And this is the script it runs:
#!/bin/zsh
set -euo pipefail
if docker info >/dev/null 2>&1; then
docker ps -q | xargs docker stop
docker container prune -f
docker volume prune -af
fi
brew update
brew upgrade -y
brew cleanup
sleep 60
osascript -e 'tell application "System Events" to restart'
If Docker is running, it stops every container and deletes the containers and all their volumes, so each project starts the day from fresh test data and nothing from yesterday keeps running. Anything I changed by hand in a local database is gone. Then it updates Homebrew and everything Homebrew installed, without asking, and removes the old versions. Homebrew installs in the foreground, so nothing is left half installed once it returns, and the script then waits a minute more as a margin. Then it restarts the Mac the way Restart in the Apple menu does, without the dialog: every app is asked to quit and can stop the restart over unsaved work, and the windows open again after I log in. If an update fails, the script stops before the wait and the Mac keeps running. The first restart asks once whether the terminal may control System Events.
I work in the Code tab of the Claude desktop app, one session per task, started in the project's folder under ~/Projects. Sessions run in auto mode, so Claude does not ask before every command, and settings.json picks the model and the effort.
I write prompts the way I would message a colleague: short, lowercase, typos left in. "start doing issue 123" or "review pr 123" is a whole prompt. Claude reads the issue, the code and its notes, and asks me only what is mine to decide, as a question with its recommended answer first. Most of my follow-ups are a word or a click: "go", "shorter", "commit and push", or a button in the box Claude leaves at the end of its turn. A follow-up that is a task of its own, Claude offers as a new session I can start with one click.
Two habits do most of the work:
dotclaude in the prompt, as in "dotclaude fix. next time ...". Claude writes the rule into CLAUDE.md or a skill, runs sync.sh, and commits and pushes it here with my prompt quoted. Many of the rules here started that way.Firefox is my default browser, so the links Claude gives me, such as a pull request or a page it made, open there. Claude does its own browsing in the browser built into the desktop app, the pane beside the chat, where I sign in once to the sites it needs. Claude in Chrome is blocked in settings.json, so Claude never drives a browser I am using.
The Air spends nights on its charger while Claude works, so it never charges past 80%. I set this once:
Charge Limit needs macOS Tahoe 26.4 or later and a Mac with Apple silicon, as Apple's support page says.
Nothing may put the Mac to sleep while Claude works. In the Claude app, Settings > Claude Code has "Keep computer awake while Claude works" and "Keep awake on battery power", and both are on. macOS itself never sleeps on a timer:
sudo pmset -a sleep 0 disksleep 0
The display still turns off after 3 minutes:
sudo pmset -a displaysleep 3
I also veto sleep altogether, so a long run is never cut short. The cost is that the Air stays awake on battery too, until I shut it down:
sudo pmset -a disablesleep 1
This shows what is in effect:
pmset -g
The weekly limit of my Claude plan resets on Saturday afternoon. /dream reads the reset time from the usage card in the app and picks its mode by itself, so I run it twice around the reset:
/dream
On the day before the reset this is an eve run. It spends most of what is left of the week overnight, but keeps back two 5-hour windows, one for my work on Saturday and one for the final run, and leaves the 5-hour window that would be open at 08:00 on Saturday untouched.
~/Projects/dotclaude and send this there. That session reads the steps from their files, while the one that ran overnight would send its whole context again with every request. dream review
It applies most steps without asking and goes through the rest one question at a time, each with the exact change, a moment where things went wrong and how that moment goes once the change is in. I answer "Add it" or "Not now", and Claude applies what I added and pushes it here. Then I carry on with my own work.
/dream in a new Code session. On the reset day this is a final run. It spends the rest of the week, down to 1 percentage point, and stops starting work an hour and a quarter before the reset, so everything reports back in time.These are the numbers from the last /dream run, or from the last time I ran the evals by hand. The rule counts come from my own sessions, and only the counts leave my computer. To run the evals on your copy:
node ~/Projects/dotclaude/test/evals.mjs
<!-- evals:start -->
| Suite | What it checks | Result |
|---|---|---|
| Hook tests | every hook, fed the input Claude Code sends it | 169 of 169 pass |
| Dream simulation | how /dream spends usage and when it stops | 49 of 49 pass |
| Usage mod tests | the line under the prompt box | 4 of 4 pass |
How often Claude kept rules from CLAUDE.md from 2026-10-17 to 2026-10-24, in 68 of my own sessions and the commits of the 6 repositories they worked in, counted by test/rules/adherence.mjs:
| Rule | Kept |
|---|---|
| No long dashes in replies | 99% (270 of 271 replies) |
| Plain quotes, arrows and dots in replies | 99% (270 of 271 replies) |
| A Prompt feedback section after each typed prompt | 91% (222 of 242 replies) |
| Commit message lines within 72 columns | 99% (336 of 337 commits) |
| No Claude attribution in commits | 100% (337 of 337 commits) |
<!-- evals:end -->
CLAUDE.md, or ask Claude to sum it up, and delete what you do not want.jq, which macOS has built in, and some need gh, GitHub's own command-line tool. git clone https://github.com/lambdakilo/dotclaude.git ~/Projects/dotclaude
a. Take only some pieces (recommended): open Claude Code and give it this prompt.
read ~/Projects/dotclaude/README.md and the files it links. explain in plain words what each piece would change for me, then copy only the ones i say yes to into ~/.claude and add their settings to my settings.json without removing anything of mine
b. Take everything. Each of your own files that it replaces is kept next to it with .bak at the end, such as ~/.claude/settings.json.bak.
~/Projects/dotclaude/sync.sh --install
To get my later changes, update your copy and then do step 2 again:
git -C ~/Projects/dotclaude pull
If you publish your own copy with sync.sh --commit, you can give the checkout a git identity of its own, such as your GitHub noreply address, so the address your other repositories use stays out of its public history:
git -C ~/Projects/dotclaude config user.name '<name>' && \
git -C ~/Projects/dotclaude config user.email '<address>'
A sync-deny.txt beside sync.sh, which git ignores, lists words that must never be published, one per line, such as the name of a project you work on: sync.sh refuses to publish a file that contains one. The command below installs a hook that refuses a commit message with one too, in your copy and in each folder that sync-mirrors.txt lists, replacing any commit-msg hook they have:
for dir in ~/Projects/dotclaude $(sed "s|^~|$HOME|" ~/Projects/dotclaude/sync-mirrors.txt 2>/dev/null); do
hook="$(git -C "$dir" rev-parse --path-format=absolute --git-path hooks/commit-msg)" &&
printf '#!/bin/sh\nexec "$HOME/Projects/dotclaude/sync.sh" --check-message "$1"\n' > "$hook" &&
chmod +x "$hook"
done
Prose, meaning CLAUDE.md, PROMPTS.md, every SKILL.md under skills/ and this README, is licensed under Creative Commons Attribution-ShareAlike 4.0 International. See LICENSE-CC-BY-SA-4.0.
Everything else is licensed under the GNU Affero General Public License, version 3 or (at your option) any later version. See LICENSE.
hooks/register.ts 81 lines1import type { EngineInterface, Register } from 'claude-code'
2
3type Usage = { figures: string; coldAt: number; offset: number }
4
5const DAY = 86_400_000
6const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
7const FIRE = '\u{1F525}'
8const ICE = '\u{1F9CA}'
9
10let usage: Usage | undefined
11let shown: string | undefined
12let isBusy = false
13
14const pad = (n: number) => String(n).padStart(2, '0')
15
16// The offset is the one usage.sh read from the host's clock, so the UTC
17// getters give local time whatever zone this environment runs in.
18const cache = (now: number, { coldAt, offset }: Usage) => {
19 const left = coldAt - now
20 if (left >= 60_000) return `${FIRE} ${Math.floor(left / 60_000)} min`
21 if (left > 0) return `${FIRE} <1 min`
22
23 const cold = new Date(coldAt + offset)
24 const isToday = Math.floor((coldAt + offset) / DAY) === Math.floor((now + offset) / DAY)
25
26 return isToday
27 ? `${ICE} since ${pad(cold.getUTCHours())}:${pad(cold.getUTCMinutes())}`
28 : `${ICE} since ${cold.getUTCDate()} ${MONTHS[cold.getUTCMonth()]}`
29}
30
31const line = async ($: EngineInterface) => usage && `${cache(await $.clock.now(), usage)}, ${usage.figures}`
32
33const show = async ($: EngineInterface) => {
34 const id = await $.session.id()
35 const { exitCode, stdout } = await $.process.run([`${$.plugin.root}/usage.sh`, '--status', id])
36 const [figures, coldAt, offset] = stdout.trim().split('\n')
37 usage =
38 exitCode === 0 && figures && coldAt && offset
39 ? { figures, coldAt: Number(coldAt) * 1000, offset: Number(offset) * 1000 }
40 : undefined
41
42 shown = await line($)
43 $.ui.status(shown)
44}
45
46// A running turn keeps refreshing the cache, so the transcript is read again.
47const tick = async ($: EngineInterface) => {
48 if (isBusy) return show($)
49 const text = await line($)
50 if (text === shown) return
51
52 shown = text
53 $.ui.status(text)
54}
55
56export const register: Register = on => {
57 on('session.start', async ($, e, next) => {
58 const started = await next(e)
59 await show($)
60 $.clock.every(15_000, () => void tick($))
61
62 return started
63 })
64
65 on('turn.start', async ($, e, next) => {
66 if (e.agentId === undefined) isBusy = true
67
68 return next(e)
69 })
70
71 on('turn.complete', async ($, e, next) => {
72 const completed = await next(e)
73 if (e.agentId === undefined) {
74 isBusy = false
75 await show($)
76 }
77
78 return completed
79 })
80}
81