A live forecast of the context window, drawn above the prompt: a weather icon, the fill percentage, and a chart of the last turns.

One source of truth for my user-level Claude Code config. Local sessions and cloud sessions (desktop app, claude.ai/code) both read it.
| Path | Holds | Becomes |
|---|---|---|
CLAUDE.md | Global working rules | ~/.claude/CLAUDE.md, through a one-line @ import |
settings.json | Output style, env vars, hooks | ~/.claude/settings.json |
agents/ | The builder, reviewer, test-author roles | ~/.claude/agents/*.md |
hooks/ | guard.py (refuses destructive git in a shared tree), config and decision watches, session_start.sh | ~/.claude/hooks/* |
lint/ | The Simplified Technical English prose linter and its hook gate | ~/.claude/lint/* |
janitor/ | A machine-wide sweep for orphaned worktrees, branches, and listeners. See janitor/GUIDE.md | not installed to ~/.claude, runs from the clone |
decisions/ | One file per ruling, cited by slug | stays in the repo |
debts/ | One file per known gap, pointing to its decision where one exists. See debts/README.md | stays in the repo |
deferred/, plans/ | Open questions and plans, not yet a ruling | stay in the repo |
actions/ | Composite GitHub Actions this repo publishes (stamp, ste-lint) | callers pin them by commit SHA |
merge/, bin/ | The merge command, and its launchers bin/merge and bin/merge.cmd | bin/ lands in ~/.claude/bin. merge/ runs from the clone |
.github/ | gates.yml, the CI that checks this repo | runs here only, never installed |
agents/, hooks/, and lint/ are the folders landed-dirs.txt names. Both installers read that file, so a new folder added there lands on every platform with no other change.
git clone https://github.com/shivinate7/claude-settings C:\src\claude-settings
cd C:\src\claude-settings
powershell -ExecutionPolicy Bypass -File .\install.ps1
Needs Windows Developer Mode (Settings > System > For developers), or an admin shell, for real symlinks. Without it, the script copies the files and installs a git post-merge hook that re-copies them after every pull on main.
mkdir -p ~/Developer
git clone https://github.com/shivinate7/claude-settings ~/Developer/claude-settings
cd ~/Developer/claude-settings
bash install.sh
Everything is a real symlink. A git pull in the clone is the whole update.
claude --cloud)A cloud VM never sees your machine's ~/.claude. In claude.ai/code, set the environment's setup script to:
curl -fsSL https://raw.githubusercontent.com/shivinate7/claude-settings/main/install.sh | bash -s -- --cloud
Leave network access at Trusted, or add raw.githubusercontent.com, github.com, and codeload.github.com to a Custom list. The script also writes a SessionStart hook that re-runs itself, so each new or resumed session re-fetches the files even from a stale snapshot. To pin cloud sessions to one commit instead of main, replace /main/ in the URL with /<sha>/, or set CLAUDE_SETTINGS_REF=<sha> as an environment variable.
/context, in any session, lists ~/.claude/CLAUDE.md and the imported file under Memory files./status shows the shiv-stylisms output style.echo $CLAUDE_CODE_SUBAGENT_MODEL. Expect sonnet.~\.claude\claude-settings-install.log for a dated line from the last run. The line reads settings.json=<mode> agents/lint=<mode>. A mode is symlink, copy, or kept. kept means that a replace failed (a file lock, say) and nothing was copied, so the old files stay in place. copy+failed (agents/lint only) means that some files were copied and at least one replace failed; that file keeps its old copy. The run prints why. Run install.ps1 again after you close the lock.Edit a file here, commit, push, open a pull request against main. .github/workflows/gates.yml runs the guard fixture suite, its mutation harness, the STE lint, and the rest of the checks named in that file. Merge only when you say so, or under a repo grant, per CLAUDE.md.
Once merged:
settings.json carries a SessionStart hook that pulls the clone at every session start. The new file is live in that same session, because Claude Code reads CLAUDE.md after SessionStart hooks finish.post-merge git hook copies the changed files after your next git pull on main, and re-runs install.ps1 itself if install.ps1 changed.merge commandOne command merges a pull request in any repo that has opted in. It claims record numbers before the merge, waits for CI on the claim commit, reverts the claim if a step fails, and then merges. Plan: plans/shared-merge-tool.md. The repo's own opt-in is the merge block of its stamp config, see "The merge tool contract" in actions/stamp/README.md.
Run it from inside the repo, with gh, git, node and Python 3 on PATH:
merge <pr> preview: what it would claim and merge. Presses nothing.
merge <pr> --confirm [--session NAME] lock, claim, push, wait, merge, move the local main, clean up.
merge --unlock remove this repo's merge lock. Reads no other state.
merge --dev ... run the checkout's own tree, not origin main
bin/merge lands in ~/.claude/bin. On Windows, bin\merge.cmd lands there too, and starts bin\merge. The installers put ~/.claude/bin on PATH. Both find this clone through CLAUDE_SETTINGS_DIR, then the @ line in CLAUDE.md under CLAUDE_CONFIG_DIR (default ~/.claude). With neither, they clone the public repo into a temporary folder. The checkout must have origin set to shivinate7/claude-settings. Any other result stops before a pull request read.origin main and runs from a temporary worktree at that SHA. It prints the SHA it ran. If it cannot prove fresh code, such as when offline, it prints a warning block and runs the code it has. --dev skips all of this and runs the checkout's own tree. Use it only to work on the tool.merge <pr> runs the claim in a temporary worktree and drops it. It prints the lock state, the state of the head's checks (green, or every pending or red check by name), the claim line, and the steps a run would take. Nothing reaches origin.--confirm. It takes the lock, then reads the required checks. If the list is empty or unreadable, it stops before the claim push. Then it checks first: it waits for the head's own checks, and a red one stops the run with nothing claimed or pushed. Only on green does it claim and push the claim, and wait for the checks on the claim SHA. It checks that each claimed number is still free on the base tip, and merges with --match-head-commit. Then it moves the local main, runs afterMerge, and deletes the head branch if merge.deleteBranch is set. A rerun on a branch that already holds its own claim resumes at the wait.merge.ignoreChecks is an optional list of {"name": "...", "reason": "..."} in the merge block. A check named there is left out of the wait, and the output names it. An entry whose name or reason is not a non-empty string refuses the config, and so does a name that is also a required check. Before the wait ends green it also reads the workflow runs of the head SHA: each must be completed with success, skipped or neutral, so a run whose jobs are not listed yet holds the wait. Then it waits 30 seconds and reads again, and the result must be green again. One window stays: a workflow that GitHub has not created yet.--unlock. The lock is the ref refs/merge-lock/<defaultBranch> on origin. It expires after twice merge.deadlineMinutes plus ten minutes (a run can wait twice), and a later run breaks an expired lock. --unlock removes it at once, for a run that died. It reads no lock state. It still reads the git root and the config. The lock records the PR, branch, host, pid and start time. Sessions should pass --session NAME with their session name. A held message names the holder and says if it is alive, dead, or unknown (another host, or an old lock). A release that fails prints one line and never hides the run's own result.Exit codes:
| Code | Meaning |
|---|---|
| 0 | A preview ran, the lock was removed, or the merge landed and every after-step passed. |
| 1 | A refusal or a failed step. The message on stderr names it. A launcher stop also exits 1. |
| 1 | The merge landed, but the local main did not move or an afterMerge command failed. The merge stays. The printed line says so. |
| 2 | The arguments are wrong, such as both a pull request number and --unlock. |
Exit 1 has two causes. Read the output to tell them apart. A line that starts merge: #<pr> merged as means that the merge landed.
CLAUDE.md: edit the file. Wrap the sentence in a <!-- rule:<slug> --> anchor if you want lint/rule_audit.py to hold you to naming its enforcement (a guard rule, a gate, a CI step, or unmechanized plus a reason) in lint/rule_mechanisms.json. All the guard rules, gates, and their design rationale live as comments and numbered decisions inside hooks/guard.py and lint/. Read there, not here, for the full list.agents/<name>.md. A project can override it with a project-level .claude/agents/<name>.md carrying the same name. See agents/builder.md and agents/reviewer.md for the two shipped roles, and CLAUDE.md's Roles section for what each is for.lint/. A guard change: edit hooks/. Re-run the installer once locally so a new symlink exists.decisions/, named by slug. Claim its number only at merge, per CLAUDE.md ("never allocate a numbered record on a branch").deferred/.python3 or python on PATH. Hook commands run under sh on Windows, so put Python on Git Bash's own PATH there. Python 3 ships with the Command Line Tools on macOS. When it is missing, run xcode-select --install. When Python is missing, the hook command exits silently, so a machine without it just runs without the gates.install.ps1 once, to replace the copies with real symlinks.~/.claude/CLAUDE.md or settings.json had content you wanted to keep. Both installers back up the old file as *.bak.<timestamp> before writing. Fold anything you want into the repo's own CLAUDE.md or settings.json, then commit.SessionStart hook the setup script installs re-fetches on every session, so this should self-heal. When it does not, check the environment's network access is not blocking the three hosts above.decisions/subagent-model-cap.md for the guarded override and its required expiry field.actions/ after this repository becomes private. Open Settings, then Actions, then General, then Access. Set "Accessible from repositories owned by the user".settings.json carries a SessionStart hook that runs a quiet git pull --ff-only in the clone, found through the @ pointer in ~/.claude/CLAUDE.md. It exits at once in cloud sessions. It never blocks a session. A failed pull falls through silently.CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=1 removes the Agent tool from every subagent, so only the main conversation fans out. A nested child's completion notice can misroute. This stalls the subagent that launched it (anthropics/claude-code#75043). A repo that needs nesting raises the depth in its own .claude/settings.json and records the risk in its CLAUDE.md.actions/ste-lint exists as a composite action. A caller repo can then lint only the lines a pull request changed, against the pull request base. It never pays down a whole document's old backlog on the first branch that touches it. See the diff-scope rationale in actions/ste-lint and decisions/ste-lint-gloss-collapse.md.janitor/GUIDE.md (decisions/the-janitor-is-one-machine-wide-sweep.md).No LICENSE file is in this repository yet. Until one is added, the default is "all rights reserved". Treat the content as personal configuration, not as licensed for reuse.
hooks/token-weather.mjs 126 lines1// Copyright 2026 Anthropic PBC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Token Weather: a live forecast of the context window, above the prompt.
5//
6// turn.complete: after each main-loop turn, read the context window's fill
7// from $.session.usage() and keep
8// the last HISTORY readings.
9// session.start: take a first reading, so the band shows before any turn.
10// ui.render (AbovePrompt): one line: icon, forecast word, percent, tokens
11// used of the window, and a block-character chart of the recent turns.
12//
13// The host reads on(...) and $.noun.method(...) from source, so they are
14// spelled literally, and helpers that take $ are top-level functions.
15
16const HISTORY = 12;
17const BARS = "▁▂▃▄▅▆▇█";
18
19// Forecast bands, by percent of the window used.
20const FORECAST = [
21// Single-width text symbols, not emoji: they line up in every terminal font.
22 { upTo: 25, icon: "☀", word: "Clear", color: "yellow" },
23 { upTo: 50, icon: "☁", word: "Cloudy", color: "cyan" },
24 { upTo: 75, icon: "☂", word: "Showers", color: "blue" },
25 { upTo: 90, icon: "☇", word: "Storm", color: "magenta" },
26 { upTo: Infinity, icon: "↯", word: "Compact soon", color: "red" },
27];
28
29// Readings: { tokens, window, percent }, oldest first.
30let readings = [];
31
32export function register(on) {
33 on("session.start", async ($, e, next) => {
34 const result = await next(e);
35 readings = [];
36 await takeReading($);
37 return result;
38 });
39
40 on("turn.complete", async ($, e, next) => {
41 const result = await next(e);
42 if (e.agentId) {
43 return result;
44 }
45 await takeReading($);
46 return result;
47 });
48
49 on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
50 if (e.hasSurvey || readings.length === 0) {
51 return next(e);
52 }
53 const { Box, Text } = $.ui.resolve(e);
54 return band(Box, Text, e.bodyColumns ?? 80);
55 });
56}
57
58async function takeReading($) {
59 try {
60 const { context } = await $.session.usage({ breakdown: "summary" });
61 if (!context || !context.window) {
62 return;
63 }
64 const tokens = context.tokens ?? 0;
65 // Measure against the auto-compact window when one is set, as /context does.
66 const { breakdown } = context;
67 const window = breakdown?.isAutoCompactEnabled && breakdown.rawMaxTokens > 0 ? Math.min(context.window, breakdown.rawMaxTokens) : context.window;
68 const percent = Math.round((tokens / window) * 100);
69 // The session.start reading is 0 before any response; drop it once real readings arrive.
70 readings = readings.filter((r) => r.tokens > 0);
71 readings.push({ tokens, window, percent });
72 if (readings.length > HISTORY) {
73 readings = readings.slice(-HISTORY);
74 }
75 $.ui.invalidate("ui.render");
76 } catch {
77 // No reading this turn; the band keeps the last one.
78 }
79}
80
81function band(Box, Text, columns) {
82 const now = readings[readings.length - 1];
83 const f = forecastFor(now.percent);
84 const trend = trendWord();
85 const parts = [
86 Text({ color: f.color, bold: true, children: `${f.icon} ${f.word}` }),
87 Text({ children: ` ${now.percent}% of context` }),
88 Text({ dimColor: true, children: ` ${short(now.tokens)} / ${short(now.window)}` }),
89 ];
90 if (columns >= 60) {
91 parts.push(Text({ dimColor: true, children: " last turns " }));
92 parts.push(Text({ color: f.color, children: chart() }));
93 if (trend) {
94 parts.push(Text({ dimColor: true, children: ` ${trend}` }));
95 }
96 }
97 return Box({ flexDirection: "row", paddingX: 1, children: parts });
98}
99
100function forecastFor(percent) {
101 return FORECAST.find((band) => percent < band.upTo) ?? FORECAST[FORECAST.length - 1];
102}
103
104// Bars scale to the busiest reading shown, so growth shows at any fill level.
105function chart() {
106 const top = Math.max(...readings.map((r) => r.tokens), 1);
107 const bars = readings.map((r) => BARS[Math.min(BARS.length - 1, Math.floor((r.tokens / top) * (BARS.length - 1)))]);
108 return bars.join("");
109}
110
111function trendWord() {
112 if (readings.length < 2) {
113 return "";
114 }
115 const delta = readings[readings.length - 1].tokens - readings[readings.length - 2].tokens;
116 if (delta > 0) return `▲ +${short(delta)} last turn`;
117 if (delta < 0) return `▼ ${short(-delta)} last turn`;
118 return "steady";
119}
120
121function short(n) {
122 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;
123 if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}k`;
124 return String(n);
125}
126