My dotfiles made with chezmoi

Personal dotfiles managed with chezmoi.
This repository is the chezmoi source directory. Files here are written in chezmoi's source-state naming format, then applied into $HOME.
dot_config/ - application configuration under ~/.configdot_codex/ - Codex configuration and agent instructionsdot_claude/ - Claude configuration and helper scriptsdot_local/bin/ - executable scripts installed into ~/.local/bin (desktop-only ones are ignored elsewhere via .chezmoiignore.tmpl)dot_gitconfig.tmpl - templated Git configurationdot_zshrc.shared - shared Zsh configurationdot_bashrc.d/shared.bash - shared Bash configuration (flyline, starship, zoxide, mise), sourced by Fedora's stock ~/.bashrc loopAnything machine-local or secret-bearing stays out of this repo (it is public) and lives in an untracked ~/.bashrc.d/work.bash, which the same loop picks up after shared.bash.
.chezmoi.toml.tmpl - chezmoi data/config template.chezmoiignore.tmpl - ignored paths for this source treeEncrypted files (~/.ssh/config) need the age key in place before the first apply, otherwise chezmoi init --apply leaves them undecrypted:
op document get "Chezmoi Age Key" --out-file ~/.config/chezmoi/key.txt
chezmoi init --apply ArzykDev/dotfiles
Login bash reads only ~/.profile (keep no ~/.bash_profile), so it must end by sourcing ~/.bashrc, which sources every file in ~/.bashrc.d:
[ -n "$BASH_VERSION" ] && [ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"
Fedora's stock ~/.bashrc already has the ~/.bashrc.d loop. On macOS, add one to ~/.bashrc, then allow Homebrew's bash before switching:
echo "$(brew --prefix)/bin/bash" | sudo tee -a /etc/shells
chsh -s "$(brew --prefix)/bin/bash"
shared.bash puts flyline on its own JSONL store, so history from a previous shell has to be imported once. Atuin's database also holds agent commands (atuin hook claude-code, dropped in 5e8d73f); filter them out on a copy first, since flyline history import takes the whole file:
sqlite3 ~/.local/share/atuin/history.db "VACUUM INTO '/tmp/atuin-user.db'"
sqlite3 /tmp/atuin-user.db "DELETE FROM history WHERE author IS NOT NULL AND author <> 'arzyk';"
flyline history import /tmp/atuin-user.db
rm /tmp/atuin-user.db
flyline is a loadable builtin, not a binary, so the import has to run from a real interactive terminal. Switching the backend also pulls in ~/.bash_history on its own, so prune that file first - agent harnesses that drove an interactive bash leave ___BEGIN___COMMAND_OUTPUT_MARKER___ wrappers and spilled heredoc bodies in it. The store is ~/.local/share/flyline/history.jsonl on Linux and ~/Library/Application Support/flyline/history.jsonl on macOS; a bad import is recoverable by deleting entries with the import's timestamp, which they all share.
Preview changes before applying them:
chezmoi diff
Apply the current source state:
chezmoi apply
Edit a managed file:
chezmoi edit ~/.zshrc
Add or refresh a file from the home directory:
chezmoi add ~/.config/example/config.toml
Check what chezmoi manages:
chezmoi managed
Use conventional commits with scopes for dotfile changes.
Prefer messages that describe the behavior change, not just that a file was updated.
Good:
feat(lazygit): use Codex for commit messages
fix(zsh): preserve SSH agent env across shells
config(git): enable difftastic pager
chore(brew): refresh package list
docs(readme): document bootstrap steps
Avoid vague subjects:
feat: update lazygit config
chore: update files
Use a body when the commit changes behavior across multiple files or when the reason is not obvious from the subject. Keep commit titles under 50 characters and body lines under 72 characters.
hooks/register.ts 70 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import { LIMIT, WARN, describe, fiveHour, notes } from './guard'
4
5const CRON_TOOLS = new Set(['CronCreate', 'CronList', 'CronDelete'])
6
7async function reading($: EngineInterface) {
8 if ((await $.store.get('off')) === true) return null
9 const w = fiveHour((await $.session.usage()).rateLimits, await $.clock.now())
10 return w && w.pct >= WARN ? w : null
11}
12
13export const register: Register = on => {
14 // Last warned percentage per window and loop; a reload forgets it and warns once more.
15 const warned = new Map<string, number>()
16
17 on('session.start', async ($, e, next) => {
18 await $.command.register({
19 name: 'usage-guard',
20 description: 'Show the 5h usage guard, or turn it off/on',
21 })
22 return next(e)
23 })
24
25 on('command.run', { command: 'usage-guard' }, async ($, e) => {
26 const arg = e.args.trim()
27 if (arg === 'off' || arg === 'on') await $.store.set('off', arg === 'off')
28 const isOff = (await $.store.get('off')) === true
29 const w = fiveHour((await $.session.usage()).rateLimits, await $.clock.now())
30 const summary = w ? describe(w) : 'No current 5h reading.'
31 return { text: `${summary} Guard is ${isOff ? 'OFF' : 'on'} (warn ${WARN}%, stop ${LIMIT}%).` }
32 })
33
34 on('session.measure', async ($, e, next) => {
35 const w = e.changed.includes('rateLimits') ? fiveHour(e.rateLimits, await $.clock.now()) : null
36 const key = `toast-${w?.reset}`
37 const level = w && w.pct >= LIMIT ? LIMIT : w && w.pct >= WARN ? WARN : 0
38 if (w && level > (warned.get(key) ?? 0)) {
39 warned.set(key, level)
40 $.ui.toast(notes(w, false).status)
41 }
42 return next(e)
43 })
44
45 on('prompt.submit', async ($, e, next) => {
46 const w = await reading($)
47 if (!w) return next(e)
48 const n = notes(w, false)
49 if (w.pct >= LIMIT && e.origin.kind !== 'task-notification')
50 return { drop: `${n.status} Resume after the reset, or run /usage-guard off to override.` }
51 const note = w.pct >= LIMIT ? n.notification : `${n.status} Keep this turn short.`
52 return next({ ...e, context: [...(e.context ?? []), note] })
53 })
54
55 on('tool.call', async ($, e, next) => {
56 const w = await reading($)
57 if (!w) return next(e)
58 const n = notes(w, e.agentId !== undefined)
59 if (w.pct >= LIMIT) {
60 if (CRON_TOOLS.has(e.tool)) return next(e)
61 return { deny: e.agentId === undefined ? n.hardStop : n.wrap }
62 }
63 const key = `${w.reset}-${e.agentId ?? ''}`
64 const ran = await next(e)
65 if (ran.deny !== undefined || w.pct - (warned.get(key) ?? 0) < 3) return ran
66 warned.set(key, w.pct)
67 return { ...ran, context: [...(ran.context ?? []), n.wrap] }
68 })
69}
70hooks/guard.ts 42 lines1import type { SessionRateLimit } from 'claude-code'
2
3export const WARN = 80
4export const LIMIT = 90
5
6export type Window = { pct: number; reset: number }
7
8// A reading whose reset has passed is from a past window: nothing to guard.
9export const fiveHour = (limits: readonly SessionRateLimit[], now: number): Window | null => {
10 const w = limits.find(l => l.kind === 'five_hour')
11 const reset = w?.resetsAt ? Date.parse(w.resetsAt) : NaN
12 return w && reset > now ? { pct: Math.round(w.percentUsed), reset } : null
13}
14
15// One-shot cron jobs on :00/:30 may fire up to 90s early, i.e. before the reset; skip those minutes.
16export const resumeAt = (reset: number) => {
17 let d = new Date(reset + 60_000)
18 if (d.getMinutes() % 30 === 0) d = new Date(d.getTime() + 60_000)
19 const pad = (n: number) => String(n).padStart(2, '0')
20 return {
21 at: `${pad(d.getHours())}:${pad(d.getMinutes())}`,
22 cron: `${d.getMinutes()} ${d.getHours()} ${d.getDate()} ${d.getMonth() + 1} *`,
23 }
24}
25
26export const describe = ({ pct, reset }: Window) =>
27 `5h window at ${pct}% (hard stop at ${LIMIT}%, resets at ${resumeAt(reset).at}).`
28
29export const notes = (w: Window, isSubagent: boolean) => {
30 const { cron } = resumeAt(w.reset)
31 const status = `Usage guard: ${describe(w)}`
32 const schedule = `schedule the resume with CronCreate (cron "${cron}", recurring false) using a handoff prompt that says where to pick up`
33 return {
34 status,
35 wrap: isSubagent
36 ? `${status} Finish the current step and return your result to the parent now; do not start new work.`
37 : `${status} Bring the work to a clean stopping point: land or note the current edit, start no new subagents or tasks. Then ${schedule}, tell the user what is unfinished, and stop.`,
38 hardStop: `${status} Hard stop: every tool but CronCreate is denied now. ${schedule[0]!.toUpperCase()}${schedule.slice(1)}, tell the user what is unfinished, and stop.`,
39 notification: `${status} Hard stop: record this result for the user, ${schedule} if not done yet, and stop.`,
40 }
41}
42