Per-model weekly rate-limit windows (the usage endpoint's weekly_scoped limits) written to ~/.claude/.usage-scoped for the statusline, with the engine holding…

Shared Claude Code configuration: slash-commands, scripts and hooks, persistent memory (MemPalace), and token optimization (RTK). Clone once, install everywhere, stay in sync.
[!WARNING] These scripts modify your system environment.
install.shperforms persistent, potentially destructive operations:
- Writes to
~/.claude/(commands, scripts, templates, mods, settings, CLAUDE.md)- Prunes
~/.claude/commands/and~/.claude/agents/: anything there without a source file in the repo is deleted on every run —~/.claude/agents/ends up holding exactly the four subagents ofagents/- Installs global packages (
graphify,mempalace,rtk)- Modifies PATH — adds
~/.local/binto~/.bashrc/~/.bash_profile/~/.profile(with confirmation, or silently with-y)- Deletes files (
graphify-out/, mempalace wings, vault folders) viaexclude-from-index.sh- Writes git hooks and config in target repos (post-commit vault sync,
pre-commitshellcheck gate in this repo,merge.ours.driver/pull.rebase false)- Auto-commits and pushes git repos (vault sync)
Read
install.shbefore running. Do not use on a machine whose~/.claude/is managed by another workflow.
This repo is the shared base. It contains everything that is useful to anyone: commands, scripts, settings templates. It does not contain personal data (no vault, no env secrets).
For a personal setup with a versioned Obsidian vault and private overrides, fork or extend this repo privately:
claude-config (this repo, public)
└── upstream ── your-claude-config (private fork)
├── vault/ # personal Obsidian vault
└── env.local # machine-specific secrets
Your private repo stays in sync with this one automatically — see Minimal setup.
curl (for auto-installing uv if missing)brew install bash — the stock 3.2 cannot run the script)install.sh doesupstream remote first: a fork that lacks the remote gets it, pointing at the public repo (CLAUDE_CONFIG_UPSTREAM_URL in install.sh), whatever its name; a checkout of the public repo itself gets none. If the sync brings changes, the script re-executes itself so the rest of the run uses the updated version, and syncs once more when those changes touched the sync script: a path added to its list arrives in the same run. Skipped, with a message, while the repo has uncommitted changes or is not on its default branch (what origin/HEAD names, else main or master: the sync commits upstream's files into the current branch)MILVUS_ADDRESS is set)~/.local/bin to persistent PATH (-y skips)~/.claude/ — commands/ and agents/ are mirrored (deployed files with no source in the repo are pruned), scripts/ and templates/ are additive, and a manifest of every file deployed (~/.claude/.deployed-files) lets the next install remove what the previous one deployed and this repo no longer holds, its own files only. agents/ holds the four subagents /feature and /audit-tests spawn, pinned to another model; a subagent dropped from the repo disappears from every machine at the next install. mods/ goes to ~/.claude/mods/ the same additive way: settings.json names the deployed paste-view in CLAUDE_CODE_PLUGIN_DIRS, so every new session shows a preview of what is pasted (a pasted text as one line that gives its size and opens it whole; for an image, a line that opens it in the system viewer under a coarse mosaic of half blocks on Windows, or a real thumbnail where the terminal draws kitty graphics, which Windows Terminal and VS Code do not) with nothing fetched from a marketplace mods/ also holds the plugins ponytail and vibe-wise, vendored at a fixed upstream commit and listed by mods/.claude-plugin/marketplace.json, the local marketplace claude-config that step 12 registers from the deployed folder~/.claude/claude-config.path; hooks, scripts/session-start.sh (SessionStart hook: newest MemPalace diary entries for the repo + head of TODO.md, ~200 tokens, and the harness integrity check scripts/harness-drift.sh: the deployed settings.json, CLAUDE.md, hooks/, agents/, commands/, scripts/ and mods/ against this clone, then the clone against the last fetched default branch of upstream (the public repo) when that remote exists, else of origin; with an upstream, the distance to origin is an information line, not an alert; a drift is shown to you at session start and on a Harness line of the statusline, nothing blocks) and scripts/session-stop.sh (Stop hook: graphify update + mining the repo into its MemPalace wing + vault sync, run detached) resolve the repo through this pointer instead of hardcoded absolute paths~/.claude/CLAUDE.md (substitutes ${VAULT_DIR})claude mcp add — mempalace (mempalace-mcp), context-mode and figma. Claude Code reads MCP servers from ~/.claude.json or a project .mcp.json only, never from settings.json. Figma authenticates over OAuth: run /mcp once inside Claude Codesettings.json — this pins the default model/effort (fable · xhigh) and points the statusline at scripts/statusline.sh on every machinesetup-rtk.shcomment-strip, syntax-check, context-monitor, cd-git-allow, api-error-alert) and fails if one is still on disk or in the deployed settings.json; then checks that the four guards (hooks/*.sh, protect-gates.js) are on disk and registered there, since a registered hook whose file is missing guards nothing. The blocking hooks now ship in hooks/ (see Quality harness below) and are registered by settings.json; comment-strip was the real cause of the "heredoc bug" (docs/pitfall.md)claude-config (the deployed ~/.claude/mods, declared by mods/.claude-plugin/marketplace.json) and installs ponytail, vibe-wise and usage-scoped from it, loaded in place with nothing fetched; installs the pinned plugins from their upstream marketplaces via the claude CLI (official context7 + frontend-design, hono); removes the upstream ponytail, caveman and vibe-wise marketplaces earlier installs registered (caveman left the harness on 2026-10-09); and warns when python3 does not run (the vibe-wise hook calls it)jq) — scripts/statusline.sh renders model, context, 5h/7d rate limits and git from the payload Claude Code pipes in, plus the active terse-mode badge, the project's VibeWise learning mode, the harness drift found at session start and the weekly per-model windows the usage-scoped mod writes to ~/.claude/.usage-scoped; the script itself makes no request and holds no login, the mod spends the credential the engine holdsstyle-toggle.sh switches it off and on.gitignore in target repos (graphify block + CLAUDE.md + mempalace.yaml + context/) using templates/gitignore.appendmempalace.yaml generation and mining into the repo's own wing, and a local CLAUDE.md re-rendered from templates/CLAUDE.project.md on every run — so a machine still holding an older generation catches up. Anything written below the template's last line is kept, and a CLAUDE.md install.sh never generated is left untouched (tests/claude-md-refresh.sh covers all four cases).gitignore management)*.sh must pass shellcheck -S warningorigin (fetch → merge → push, retried on races) via scripts/vault-sync.shinstall.sh --only claude runs steps 4 and 7–14 only; install.sh --only repos runs steps 15–19 only. Steps 1–3, 5 and 6 (sync, dependencies, repo pointer, MemPalace health) run under either scope — both halves need them.
install.sh on a fresh clone already gives you the shared config. Two more things turn it into a personal setup: a private repo to hold your vault and overrides, and Obsidian to read what Graphify writes.
# Clone the public repo as your private base
git clone https://github.com/RemiAsselin42/claude-config my-claude-config
cd my-claude-config
# Point origin to your private repo, keep public as upstream
git remote rename origin upstream
git remote add origin https://github.com/<you>/my-claude-config
git push -u origin main
From then on, scripts/sync-upstream.sh pulls shared files from upstream into your private repo without touching personal files (vault/, env.local, .claude/). It runs at the start of every install.sh and nowhere else: no hook calls it between installs, so the script's 8-hour debounce (~/.claude/.upstream-sync-stamp) only matters if you wire one up yourself. It never touches a fork with uncommitted changes on a synced path, and it cannot sync an unreachable upstream: in both cases it exits 3 with the reason on stderr, and install.sh prints a yellow ⚠ upstream sync skipped — the files it then deploys come from the fork as it is, possibly behind upstream — instead of the green ✓ upstream synced. A file upstream dropped or renamed leaves the fork at the next sync, whenever that happened: the pass compares the fork's tracked files on the synced paths with upstream/main and removes those upstream's history knows; a file upstream never had, the fork's own, stays.
The public repo ships no vault/ — install.sh creates it in your private repo and writes, per indexed repo, Projets/<repo>/ with the graph report, the file tree, a <repo>.canvas community map and one note per graph node. To read it: Obsidian → Open folder as vault → select <your-repo>/vault.
scripts/vault-sync.sh commits it and reconciles with origin (fetch → merge → push) at the end of every install and every session, so several machines can write to the same vault.
Nothing below is required — defaults work.
| Where | Option | Effect |
|---|---|---|
| CLI | install.sh -y | Non-interactive: keeps each repo's current indexing state, auto-accepts the PATH change |
| CLI | install.sh -v | Verbose installer output |
| CLI | install.sh --only claude | Claude side only: ~/.claude files, settings, MCP servers, plugins; skips the repos |
| CLI | install.sh --only repos | Repos side only: graph, community names, vault, MemPalace wings; skips the Claude files |
| Prompt | PATH | Asked once, to add ~/.local/bin to ~/.bashrc / ~/.bash_profile / ~/.profile |
| Prompt | Repo selection | Which sibling git repos to index (graphify + MemPalace + vault) |
env.local | MEMPALACE_EMBEDDING_MODEL | embeddinggemma (default, multilingual) or minilm (English-only, faster) |
env.local | MEMPALACE_PALACE_PATH | Move the palace off ~/.mempalace/palace (small system drive, synced folder) |
env.local | GRAPHIFY_LABEL_BACKEND / _MODEL | Which LLM names the graph communities (default: the claude CLI, no API key) |
env.local | GRAPHIFY_DEEP_EXTRACT | LLM re-extraction adding INFERRED edges — slow, billed on paid backends |
env.local | MILVUS_ADDRESS / MILVUS_TOKEN / OPENAI_API_KEY | Enables the Zilliz semantic-search MCP server |
Each key is documented inline in env.local.template.
claude-config/
├── install.sh # Main installation script
├── env.local.template # Machine-specific variables (Figma key, embedder, label backend…)
├── CLAUDE.md # Global instructions for Claude Code
├── settings.json # Permissions, hooks, effort level, attribution
├── mempalace.yaml # This repo's own MemPalace wing + mining exclusions
├── .graphifyignore # Keeps vault/ (generated) out of this repo's own graph
├── .gitignore # env.local, vault/, context/, graphify-out/: the per-machine and generated side
├── .gitattributes # LF everywhere; vault/ and graphify-out/ merge "ours", no eol conversion
├── README.md # This file
├── README.fr.md # Same, in French
│
├── .github/workflows/
│ ├── arch-gates-python.yml # Reusable CI workflow: import cycles + layer contracts of a Python package
│ ├── arch-gates-frontend.yml # Reusable CI workflow: import cycles + layer contracts under src/ (dependency-cruiser)
│ ├── baseline-ratchet.yml # Reusable CI workflow: a gate baseline may only shrink
│ ├── mutation-gate.yml # Reusable CI workflow: mutmut on what [tool.mutmut] names, then the not-killed mutants against the baseline
│ ├── quality-python.yml # Reusable CI workflow: ruff C901 and jscpd on a Python package, then both against their baselines
│ ├── quality-frontend.yml # Reusable CI workflow: the project's eslint with the complexity rule and jscpd under src/, then both against their baselines
│ └── ci.yml # This repo's own CI: shellcheck + every test under tests/, on ubuntu and macos
├── gates/python/
│ ├── check_imports.py # Python cycles + layers gate (grimp), run by arch-gates-python.yml at the pinned tag
│ ├── check_mutation.py # Mutation gate: the mutants the tests do not kill (mutmut) may only shrink, run by mutation-gate.yml
│ └── check_quality.py # Complexity (ruff C901, eslint complexity) and duplication (jscpd) gates: both may only shrink, run by quality-*.yml
├── agents/ # Subagents → ~/.claude/agents/ (mirrored), pinned to another model, spawned by /feature and /audit-tests
│ ├── plan-reviewer.md # Reviews the plan against the spec before any code, read-only
│ ├── spec-tester.md # Writes the acceptance tests from the spec, before the code exists
│ ├── diff-reviewer.md # Adversarial review of the diff against spec, tests and gate output, read-only
│ └── test-auditor.md # Interprets a test-ablation report, one verdict per measured fact from a closed grid, read-only; spawned twice by /audit-tests
├── commands/ # Slash-commands → ~/.claude/commands/
├── hooks/ # PreToolUse guards → ~/.claude/hooks/ (Bash and PowerShell tools)
│ ├── protect-gates.js # Blocks Claude's edits to gate configs, baselines, workflows, hook bypasses, merge, labels, ref deletion, the deployed harness under ~/.claude (hard), a project's own .claude/agents and commands (soft)
│ ├── destructive-guard.sh # rm -rf on broad paths, git reset --hard, git clean, forced checkout (vendored, cc-safe-setup)
│ ├── branch-guard.sh # Push to main/master, force push (vendored, cc-safe-setup)
│ └── secret-guard.sh # git add of .env / keys / credentials (vendored, cc-safe-setup)
├── scripts/ # Utility scripts → ~/.claude/scripts/
│ ├── baseline-ratchet.cjs # Compares baselines between two refs; run by the workflow above (.cjs: callers may be ESM packages)
│ ├── test-ablation.cjs # Statement ablation audit of one TypeScript source under Vitest, in a temp copy of the project: the measure behind /audit-tests (.cjs: the audited package may be ESM)
│ ├── repo-identity.sh # Shared lib: canonical_repo_name()
│ ├── session-start.sh # SessionStart hook: starts the MemPalace daemon, diary + TODO.md head, one line when MemPalace is down, the harness drift
│ ├── harness-drift.sh # Integrity of the deployed harness: deployed ≠ clone, clone ≠ origin/<default branch>; run by session-start.sh, read by the statusline
│ ├── session-stop.sh # Stop hook: graphify update + wing mine + vault sync
│ ├── statusline.sh # Statusline: model, context, rate limits (+ the weekly per-model windows from .usage-scoped), mode, VibeWise, harness drift, git
│ ├── style-toggle.sh # Switch the ponytail terse mode: on ⇄ off, status
│ ├── vibe-toggle.sh # VibeWise learning mode of the current project: on ⇄ off, status
│ ├── setup-rtk.sh # Install RTK
│ ├── sync-upstream.sh # Sync shared files from upstream remote
│ ├── sync-graph-to-vault.sh # Sync Graphify → Obsidian vault
│ ├── vault-sync.sh # Commit + fetch/merge/push the vault (multi-machine safe)
│ └── exclude-from-index.sh # Remove a repo from graphify + mempalace
├── templates/
│ ├── CLAUDE.project.md # Per-repo CLAUDE.md, re-rendered on every install
│ ├── gitignore.append # .gitignore entries appended by install.sh
│ └── gates/ # What /init-gates creates in a repo: dependency-cruiser config, the CI callers of the three gate families
├── mods/ # Vendored Claude Code plugins and mods → ~/.claude/mods/ (copied by install.sh, nothing fetched from a marketplace)
│ ├── .claude-plugin/ # marketplace.json: the local marketplace `claude-config` install.sh registers from ~/.claude/mods, listing ./ponytail, ./vibe-wise and ./usage-scoped
│ ├── paste-view/ # Mod: pasted images and long texts previewed above the prompt (Amorfx/claude-paste-view at a71ba10, MIT, Windows added; loaded through CLAUDE_CODE_PLUGIN_DIRS); claude plugin test mods/paste-view
│ ├── ponytail/ # Plugin: lazy senior dev mode, the default terse mode (DietrichGebert/ponytail 4.8.4 at 16f2980, MIT; one matcher added so the harness agents stay out of it)
│ ├── usage-scoped/ # Mod (local): after each measure, the weekly per-model rate-limit windows → ~/.claude/.usage-scoped for the statusline, the engine holding the credential; claude plugin test mods/usage-scoped
│ └── vibe-wise/ # Plugin: learning mode per project (nykooi1/vibe-wise 0.1.43 at c5fc8d8, MIT, as is)
├── docs/
│ └── pitfall.md # Append-only log of traps Claude Code hit in this repo
└── tests/
├── claude-md-refresh.sh # Self-check for the per-repo CLAUDE.md refresh
├── statusline.sh # Pins the statusline line format against a fixture payload, the Week <model> segments from a .usage-scoped fixture included
├── usage-scoped.sh # The usage-scoped mod's wiring: marketplace entry, MODS_PLUGINS, enabledPlugins, manifest and hooks modules (the mod itself: claude plugin test mods/usage-scoped)
├── harness-drift.sh # harness-drift.sh on a throwaway clone and config dir: each drift kind named, tolerated keys silent, upstream vs origin, the marker, session-start.sh's JSON
├── deploy-manifest.sh # install.sh's deploy manifest: a file the repo dropped leaves ~/.claude at the next install, the owner's files and foreign paths never
├── vibe-toggle.sh # vibe-toggle.sh on throwaway projects (lookup, on/off, CRLF, symlinks), the VibeWise statusline line, the plugin's wiring
├── style-toggle.sh # style-toggle.sh: ponytail on (plugin config + session flag), off, status, any other plugin name refused
├── vibe-toggle-write.sh # What vibe-toggle.sh writes: only the plugin's markers, line endings and a missing final newline kept, a failed write leaves the notes whole
├── legacy-hooks.sh # install.sh must remove the dropped cc-safe-setup hooks and notice a leftover
├── install-scope.sh # install.sh --only: usage names both halves, bad values are refused, the guard answers right
├── sync-upstream.sh # The upstream sync on two throwaway repos: dirty or unreachable = exit 3 (skipped), clean = pulled and committed, a path added to the list = brought by install.sh's second pass, a file renamed upstream = gone from the fork
├── readme-structure.sh # Both READMEs against git ls-files: every tree entry tracked, every tracked entry in the tree, command table = commands/
├── workflows-yaml.sh # Every workflow and gate template parses as YAML (js-yaml): a broken one runs nothing and reaches no PR
├── mempalace-health.sh # install.sh looks for venv holders before uv, trusts an import over a version, fails closed on a broken venv; session-start.sh starts the daemon and says when MemPalace is down
├── hooks.test.js # Every guard in hooks/, fed Bash and PowerShell payloads (node --test "tests/*.test.js")
├── ponytail-subagent.test.js # The SubagentStart matcher of the vendored ponytail skips the harness agents declared in agents/ and no other subagent
├── baseline-ratchet.test.js # The ratchet on real baselines from a pilot repository
├── gates-template.test.js # The dependency-cruiser template on a toy project: layers, cycle, module in no layer, path alias
├── test-ablation.test.js # The ablation script on toy Vitest projects in os.tmpdir(): facts, kill kinds, flaky and orphan tests, determinism, isolation, exit 2, one toy per supported major (vitest@5.0.4, 3.2.7 and 4.1.11 with their Vite, typescript@6.0.3, installed by npm; stub packages for the version gate)
├── python/conftest.py # load_gate(name): a gate script loaded from gates/python by file, the way CI runs it
├── python/test_check_imports.py # The Python gates on toy graphs, temp trees and end to end (uv run --no-project --with grimp==3.14 --with pytest pytest tests/python)
├── python/test_check_mutation.py # The mutation gate on written .meta files, and on a real mutmut run of a toy package (Linux and macOS: mutmut refuses native Windows)
├── python/test_check_quality.py # The complexity and duplication gates on reports cut from real runs, and on real ruff, eslint and jscpd runs over toy projects
└── fixtures/ # Real baselines and pyproject.toml from a pilot repository, anonymized
hooks/register.ts 77 lines1import type { Register } from 'claude-code'
2
3// usage-scoped: the per-model weekly rate-limit windows, which the statusline
4// payload does not carry (it has five_hour and seven_day alone). After each
5// session.measure, outside its dispatch, the OAuth usage endpoint is read with
6// the session's credential held by the engine ($.session.authorize() answers an
7// opaque handle, $.http.fetch spends it; the secret never reaches this module),
8// and its `limits` of kind weekly_scoped are written to <config dir>/.usage-scoped
9// as one "label<TAB>percent<TAB>resets_at (epoch seconds)" line per window, only
10// when the text changed. scripts/statusline.sh renders one "Week <label>" segment
11// per line. No bearer credential (logged out, or an API key) is a state, not a
12// failure: no request is made and the file is emptied once, so a file left by an
13// earlier login does not keep its segments on screen. Anything wrong (a refused or
14// failed request, a body that is not JSON, no config dir) writes nothing and
15// raises nothing.
16//
17// claude plugin validate mods/usage-scoped
18// claude plugin test mods/usage-scoped
19
20const URL = 'https://api.anthropic.com/api/oauth/usage'
21
22type Limit = { kind?: unknown; percent?: unknown; resets_at?: unknown; scope?: { model?: { display_name?: unknown }; surface?: unknown } }
23
24/** The window's name: the model's display name, else the surface; every control and format character (U+202E, U+200B...) removed, the ends trimmed; '' when neither leaves anything. */
25const label = (l: Limit) =>
26 [l.scope?.model?.display_name, l.scope?.surface]
27 .map(v => (typeof v === 'string' ? v.replace(/\p{C}/gu, '').trim() : ''))
28 .find(v => v !== '') ?? ''
29const percent = (v: unknown) => Math.min(100, Math.max(0, Math.round(Number(v) || 0)))
30/** Epoch seconds from an ISO 8601 string, epoch milliseconds or epoch seconds; 0 otherwise, never negative. */
31const reset = (v: unknown) => {
32 const ms = typeof v === 'string' ? Date.parse(v) : typeof v === 'number' ? (v > 1e11 ? v : v * 1000) : NaN
33 return Number.isFinite(ms) ? Math.max(0, Math.floor(ms / 1000)) : 0
34}
35
36export const register: Register = on => {
37 let last: string | undefined
38 let seq = 0
39
40 on('session.measure', ($, e, next) => {
41 $.clock.after(0, async () => {
42 // Refreshes may overlap; only the latest one started may write, so an
43 // answer that arrives late never puts an older text over a newer one.
44 const mine = ++seq
45 try {
46 // An empty variable counts as unset, as the statusline's ${CLAUDE_CONFIG_DIR:-$HOME/.claude} does.
47 let dir = await $.env.get('CLAUDE_CONFIG_DIR')
48 if (!dir) {
49 const home = (await $.env.get('HOME')) || (await $.env.get('USERPROFILE'))
50 if (!home) return
51 dir = `${home}/.claude`
52 }
53 const auth = await $.session.authorize()
54 let text = ''
55 if (auth?.kind === 'bearer') {
56 const res = await $.http.fetch(URL, { auth: auth.handle, headers: { 'anthropic-beta': 'oauth-2025-04-20' } })
57 if (!res.ok) return
58 const limits: unknown = JSON.parse(res.text)?.limits
59 if (!Array.isArray(limits)) return
60 text = (limits as Limit[])
61 .filter(l => l?.kind === 'weekly_scoped' && label(l) !== '')
62 .map(l => `${label(l)}\t${percent(l.percent)}\t${reset(l.resets_at)}\n`)
63 .join('')
64 }
65 // Nothing awaits between this check and the write, so no newer refresh lands in between;
66 // a write still in flight when a newer one has landed leaves `last` to the newer one.
67 if (mine !== seq || text === last) return
68 await $.fs.write(`${dir}/.usage-scoped`, text)
69 if (mine === seq) last = text
70 } catch {
71 // A failed refresh leaves the last file as it is; the next measure tries again.
72 }
73 })
74 return next(e)
75 })
76}
77