Files
dots/common/.config/claude

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 push, gh pr merge / close, and reads of secret files; ask before gh pr create and git rebase; allow gh reads, gh pr edit, gh workflow run and gh run rerun; git commit is left to hooks/git-guard.py, which exempts listed repos), default model, enabledPlugins, vim mode, dark theme, effortLevel, fullscreen TUI, statusLine, hooks, worktree defaults, a generic autoMode.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 reads autoMode from user/managed settings anyway (never from a project's .claude/settings*.json).
  • statusline.py — rich status line (dir, git, unresolved PR review threads, 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 (alt+j/k and ctrl+shift+j/k scroll by line — kitty's own kitty_mod+j/k scroll only its main-screen scrollback and pass the key through to the fullscreen TUI's alternate screen, so they do not shadow these; dropped once by mistake on 2026-09-15), plus ctrl+x v for the verbose transcript view (app:toggleTranscript, same keystroke as opencode's <leader>v; the default ctrl+o still works, and ctrl+e inside 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 .editorconfig section that matches the file. Wired to Edit|Write and Bash — 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 on Bash: structurally blocks anything that commits, rewrites history or discards working-tree changes — git commit / push / revert / cherry-pick / rebase -i|--autostash / merge (not --abort), clean / reset in 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, and gh pr merge / close, gh pr create without --head (gh would push the branch itself) and gh pr edit beyond --title / --body — wherever the flags sit (git -C . push, /usr/bin/git push, sh -c 'git push'), which the prefix-matched deny rules can't. A plain rebase and its --continue / --abort pass: Claude rebases and fixes conflicts when asked (CLAUDE.md), and settings.json asks before each git rebase. 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). Repos listed in ~/.claude/git-guard-exempt (host-local, not in this repo — it names projects; one main checkout per line, worktrees included): commit and merge pass there — judged from the hook input's cwd, every cd before the call and git -C — except commit --amend / -a. A user-scope deny cannot be lifted for one repo, so git commit is off the deny list: the hook is its only structural block.
  • hooks/notify.py — Notification hook (idle_prompt|permission_prompt): notify-send desktop notification when Claude is waiting on you; no-op on a headless host.
  • ../tmux/scripts/agent-state.sh — UserPromptSubmit (busy) / Notification (input) / Stop (done) / StopFailure (error) / SessionEnd (off) hooks, all async: marks the tmux pane this session runs in (@agent pane option, drawn as ✓ ? ! after the window name by tmux.conf; visiting the pane greys it out, the next prompt removes it) and buzzes the phone for a new mark on a pane not in view (termux-notification, else a bell). Claude sessions run in daemon-spawned workers with no tmux env, so the script finds the pane by the session name Claude puts in the pane title. Shared with codex (~/.codex/hooks.json, untracked), pi and opencode. No-op outside tmux.
  • 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 still link.sh (heals identical copies, warns otherwise).
  • hooks/local-commands.py — UserPromptExpansion hook: TUI-side slash commands. When you type /x and skills/x/local.py exists, 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 a local.py expand as usual. Caveat baked into the design: the hook fires after the skill body expanded, so a skill with a local.py must not also act via a ! inline command.
  • skills/ — custom Agent Skills. See Skills below.
  • link.sh — idempotent bootstrap that creates the ~/.claude symlinks below.
  • ../git/claude-commit-msg + ../git/hooks/prepare-commit-msg — the commit-message handoff. A session pipes its suggestion into claude-commit-msg save (run inside the target repo), which stores it as its own file under $(git rev-parse --git-path CLAUDE_COMMIT_MSG.d)/ with the paths it staged; on the next git commit / lazygit C the 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 list shows what is pending. The old single CLAUDE_COMMIT_MSG file 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 global core.hooksPath, which would override a project's own hooks. The prefill only runs for an editor commit: lazygit's c panel passes the message to git directly, so the hook skips and the suggestion stays pending.

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 commit-message prefill hook. New clones/inits get it
#    from ~/.config/git/template (init.templateDir) once that template hook is a symlink to it too.
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.

Tracked files in ~/.claude link straight at the repo copy, not through ~/.config/claude: Claude Code follows one symlink hop and then replaces the file atomically (temp + rename), so the old two-hop chain lost its middle link to a plain file on every settings write (/model, effort, plugins, /config) and the repo stopped seeing changes. With one hop the rename lands in the checkout. Dirs (hooks, skills) still go through ~/.config/claude.

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 with file: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 with wl-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 recent git log (e.g. this repo's [Scope] summary), drafts a matching message, prints it, and saves it for the prepare-commit-msg hook (lazygit C prefills it). 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 /focus choice for new sessions) plus verbose and showThinkingSummaries, written to the project's .claude/settings.local.json (machine-local, never committed) rather than the tracked settings.json, so a toggle never shows up as a dots diff. Runs TUI-side — local.py via hooks/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 @claude request — git adds them, drafts the commit message (/commit-msg style) 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 (via ScheduleWakeup). 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; needs gh authed and the session left open. Each cycle it prints when it will re-check and that Esc (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/.