Claude Code config
Portable Claude Code customizations, synced via these
dots. Claude Code still uses its default ~/.claude/ directory; we surface the tracked
files below into it via per-file/dir symlinks, so edits made through Claude write straight
back into this repo.
What's tracked here
settings.json— global settings: permissions (deny git commit/push,gh prwrite ops, and reads of secret files), defaultmodel,enabledPlugins, vim mode, dark theme,effortLevel, fullscreen TUI,statusLine,hooks,worktreedefaults, a genericautoMode.environment, … Nothing in here may name a specific project, path or remote: this file is published by the public mirror, and the auto-mode classifier only readsautoModefrom user/managed settings anyway (never from a project's.claude/settings*.json).statusline.py— rich status line (dir, git, model, context %, cost, disk — system metrics live in the tmux bar). See StatusBar.md for what each segment and colour means.keybindings.json— custom keybindings: vim-style scroll/navigation, plusctrl+x vfor the verbose transcript view (app:toggleTranscript, same keystroke as opencode's<leader>v; the defaultctrl+ostill works, andctrl+einside the view expands everything).CLAUDE.md— global user preferences applied to every project.hooks/format.py— PostToolUse hook: formats edited files by extension (stylua / ruff / prettier / gofmt / rustfmt / fish_indent / shfmt / taplo). Every formatter is opt-in: stylua, ruff, prettier and taplo run only where the project has their config file (found walking up); gofmt and rustfmt gate on a project marker (go.mod/Cargo.toml); fish_indent and shfmt gate on an.editorconfigsection that matches the file. Wired toEdit|WriteandBash— under auto mode Claude edits through the shell, so the Bash branch scans the command for path-like tokens and formats the ones modified in the last 30 s. No-ops if the formatter isn't installed, and always exits 0.hooks/git-guard.py— PreToolUse hook onBash: structurally blocks anything that commits, rewrites history or discards working-tree changes —git commit/push/revert/cherry-pick/rebase/merge(not--abort),clean/resetin any form that moves HEAD or unstages everything (reset -- <paths>,reset HEAD <path>stay allowed) /restore <paths>(--staged <path>is fine,--staged .is not) /checkout --/checkout ./stash drop|pop|clear/branch -D|-f|-M, andgh pr merge/create/close/edit— wherever the flags sit (git -C . push,/usr/bin/git push,sh -c 'git push'), which the prefix-matched deny rules can't. The discard group is what protects parallel sessions sharing one checkout. Exit 2 + a one-line reason. Heredoc bodies are skipped on purpose (file contents). Regression matrix:bash ~/.claude/hooks/git-guard-test.sh(a file on purpose — typed inline, the commands under test would trip the live hook).hooks/notify.py— Notification hook (idle_prompt|permission_prompt):notify-senddesktop notification when Claude is waiting on you; no-op on a headless host.hooks/drift-check.sh— SessionStart + UserPromptSubmit hook, read-only: fails loudly when a~/.config/claude/*file has broken out of the repo into a plain file, or~/.claude/*no longer resolves into the dots repo. That is what Claude Code's atomic settings rewrite does through the stow layer — on 2026-09-15 it silently reverted the live config to a stale snapshot for an hour, dropping four hook events. At session start the warning is shown to you (exit 2, session goes on); on each prompt any drift is fed to Claude as context so it reports it. Fix is stilllink.sh(heals identical copies, warns otherwise).hooks/local-commands.py— UserPromptExpansion hook: TUI-side slash commands. When you type/xandskills/x/local.pyexists, the hook runs it with the arguments, shows its output to you and blocks the expansion (exit 2), so no prompt reaches the model and it costs nothing. Skills without alocal.pyexpand as usual. Caveat baked into the design: the hook fires after the skill body expanded, so a skill with alocal.pymust not also act via a!inline command.skills/— custom Agent Skills. See Skills below.link.sh— idempotent bootstrap that creates the~/.claudesymlinks below.../git/claude-commit-msg+../git/hooks/prepare-commit-msg— the commit-message handoff. A session pipes its suggestion intoclaude-commit-msg save, which stores it as its own file under$(git rev-parse --git-path CLAUDE_COMMIT_MSG.d)/with the paths it staged; on the nextgit commit/ lazygitCthe hook prefills the editor with the message(s) whose paths are part of that commit and consumes them (used/). Parallel sessions therefore never overwrite each other, and committing one session's files gets that session's message.claude-commit-msg listshows what is pending. The old singleCLAUDE_COMMIT_MSGfile is still honoured. Both tracked here (deployed to~/.config/git/); the hook is installed per repo — see setup step 4 — and deliberately not via a globalcore.hooksPath, which would override a project's own hooks.
The settings.json hook commands for the newer scripts are wrapped in [ -f … ], so a host that
has pulled the repo but not yet re-stowed just skips them instead of blocking every Bash call.
Paths inside settings.json reference ~/.claude/... via $HOME/PATH (/usr/bin/env python3 ~/.claude/...), so they work regardless of the username, python location, or where the
dots repo is cloned (~/.dots, …).
What is not tracked (stays local, per device)
Everything else under ~/.claude/ is machine-specific or secret and must never be committed:
credentials (.credentials.json), session history, projects/ (transcripts + memory),
caches, and the plugins/ cache/binaries.
Setup on a new device
# 1. Deploy the dotfiles repo — install.sh stows ~/.config/claude AND runs link.sh for you
cd ~/.dots && ./install.sh
# 2. Launch Claude and log in once (credentials are NOT synced)
claude
# 3. Reinstall the plugins from settings.json -> enabledPlugins
# (typescript-lsp, frontend-design — anthropics/claude-plugins-official) via /plugin.
# Optional: install any formatters you want the hook to use (stylua, ruff, prettier, …),
# and libnotify (notify-send) on GUI hosts for hooks/notify.py.
# 4. In every repo you commit in: install the one-shot commit-message prefill hook
ln -sf ~/.config/git/hooks/prepare-commit-msg (git rev-parse --git-path hooks)/prepare-commit-msg
install.sh deploys ~/.config/claude (via stow) and then runs link.sh automatically. You only
need to run link.sh by hand (bash ~/.config/claude/link.sh) if a link later gets clobbered
(e.g. Claude's /config replaces one with a plain file) — it's idempotent and self-healing.
A new skill/hook added to the repo shows up after a re-stow — ./install.sh (or dotsync) links
the new file into ~/.config/claude, which link.sh then surfaces into ~/.claude.
Skills
Custom skills live in skills/<name>/SKILL.md (the directory name is the /command) and are
linked in as ~/.claude/skills. All are disable-model-invocation: true — explicit / only,
so Claude never auto-triggers them. Run them inside the repo whose branch you're working on.
/review-branch [base]— reviews the current branch's diff (vs its merge-base with the base branch) for 🐞 bugs / 🔒 security / ⚡ optimizations / 📖 readability, reports grouped findings withfile:line, then applies the readability + safe fixes after you approve. Never commits. Borrows/code-review's discipline: CLAUDE.md-aware, changed-lines-only, verified bugs with a false-positive filter (skips what linters/CI catch and pre-existing issues). Ends by pointing you at the relevant built-in follow-ups (/security-review,/code-review,/verify, …)./pr-description [base]— auto-detects this repo's GitHub PR template (.github/pull_request_template.md, …), fills it from the branch diff + commits, prints a copy-paste markdown block, and copies it to the clipboard withwl-copy. Leaves verification checkboxes for you; never creates/pushes the PR./commit-msg [hint]— reads the staged diff, infers the repo's commit style from recentgit log(e.g. this repo's[Scope] summary), drafts a matching message, prints it, and copies it to the clipboard withwl-copy. Never stages or commits — draft only./verbose [on|off|status]— flips the persisted verbose view for the current project:viewMode(verbose/default— the key that actually decides the view and beats a sticky/focuschoice for new sessions) plusverboseandshowThinkingSummaries, written to the project's.claude/settings.local.json(machine-local, never committed) rather than the trackedsettings.json, so a toggle never shows up as a dots diff. Runs TUI-side —local.pyviahooks/local-commands.py, no model turn — with the SKILL.md body as the fallback on a host where the hook isn't deployed. The live toggle is a keypress:ctrl+x v/ctrl+o./pr-loop [pr]— babysits the current branch's PR in a self-paced loop: fixes review comments that don't need your input — from any reviewer (Codex, Claude, humans) plus any@clauderequest —git adds them, drafts the commit message (/commit-msgstyle) and marks each thread with a reaction instead of an "Addressed — …" reply: 👍 fixed, 👀 parked for your decision, 👎 a bot false positive (bots only, always with a one-line why). Then pings you to commit & push and re-checks every ~10 min (viaScheduleWakeup). Resolves threads once their fix is pushed — dismissals right away, since there's nothing to land. Stops when no unresolved comments remain or the automated reviewers (Codex/Claude) hit their limit. Prep-only — never commits/pushes/merges; needsghauthed and the session left open. Each cycle it prints when it will re-check and thatEsc(between cycles) stops it; on the last cycle it prints a clear "Finished — no more active issues" and does not reschedule.
Adding more config later
Drop the file/dir under .config/claude/ here, add its name to the items list in link.sh,
and re-run bash ~/.config/claude/link.sh. For example commands/ or agents/.