Shows how long the current turn has been running, then how long the last turn took, in the prompt footer

brew install just)brew install jq) — used by claude-settings and claude-statusline.shjust formulae) — used by just lintTo set up all of the files as symlinks in your home directory:
just all
Or set up individual components:
just git # git config and global gitignore
just zsh # zshrc and zsh.d
just ssh # ssh config and per-machine key (see below)
just ghostty # ghostty terminal config and shaders
just atuin # atuin shell history config
just claude # claude AI config (see below)
just veer # veer global rules + PreToolUse hook (see below)
just formulae # install homebrew formulae (git-lfs, ruff, shellcheck, sumpig, veer)
The just ssh recipe sets up ~/.ssh/config as a symlink and ensures a per-machine ~/.ssh/id_ed25519 key exists.
The model is one key per machine. Private keys never sync between machines — each Mac generates its own on first run and the public key gets registered with the services that need it (GitHub, any servers). When you lose a machine, you revoke just that one key.
On first run on a new Mac, the recipe will:
~/.ssh with 0700 perms~/.ssh/config to ssh/config in this repo~/.ssh/id_ed25519 exists, run ssh-keygen -t ed25519 interactively (pick a strong passphrase and stash it in the macOS Passwords app so it syncs via iCloud)ssh-add --apple-use-keychainSubsequent runs are idempotent: if the key already exists, the recipe just prints the public key for reference.
The ssh/config here uses AddKeysToAgent yes and UseKeychain yes, so once the passphrase is cached in the login keychain you won't be re-prompted on subsequent sessions.
The just claude recipe sets up:
~/.claude/CLAUDE.md~/.claude/skills/~/.claude/settings.json with values from claude-settings-patch.json (deep-merged via jq, preserving any existing keys)_claude_marketplaces and installs the plugins in _claude_plugins (superpowers, mattpocock-skills, and the tednaleid/claude-plugins set), updating any that are already installedThe claude-settings recipe merges keys from claude-settings-patch.json into the live settings file. This is a one-way patch (dotfiles -> live settings), so machine-specific keys in settings.json are preserved.
claude-statusline.sh is a status line script referenced by the patched settings. It displays the current directory, git branch, model name, and color-coded context token usage. The context indicator uses a smooth truecolor gradient that reads colors from the active Ghostty theme palette.
An optional env var controls the context gradient:
CLAUDE_CONTEXT_GRADIENT -- three comma-separated percentages: where the color stops being green, where it reaches yellow, and where it reaches red. Default 15,40,70.just veer does two things:
veer_config.toml to ~/.config/veer/config.toml -- the global rules veer applies to every project unless a project's own .veer/config.toml overrides them.veer install --global to register the veer check PreToolUse hook in ~/.claude/settings.json. Idempotent; re-runs update the hook entry to the current version/flags. Restart Claude Code after the first install for the hook to take effect.The veer binary must be on PATH for step 2 -- run just formulae (or just all) on a fresh machine.
Because the live config is a symlink into the repo, veer add --global and veer remove --global write through to veer_config.toml. CLI edits land in the dotfiles repo automatically, ready to commit.
On the first machine to adopt this, move the existing real file in before running the recipe (the symlink helper skips if the destination already exists):
mv ~/.config/veer/config.toml ./veer_config.toml
just veerhooks/register.ts 50 lines1// ABOUTME: turn-timer mod: a label in the prompt footer's mode area with the running turn's elapsed
2// ABOUTME: time, then the last turn's duration once it ends. Subagent turns are ignored.
3
4import { atom, read, update } from 'claude-code'
5import type { Register, Timer } from 'claude-code'
6
7const label = atom({ plugin: 'turn-timer', key: 'label' } as const, null)
8
9export const formatDuration = (ms: number): string => {
10 const seconds = Math.floor(ms / 1000)
11 if (seconds < 60) return `${seconds}s`
12 const minutes = Math.floor(seconds / 60)
13 if (minutes < 60) return `${minutes}m${String(seconds % 60).padStart(2, '0')}s`
14 return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`
15}
16
17export const register: Register = on => {
18 let tick: Timer | undefined
19
20 on('turn.start', async ($, e, next) => {
21 tick?.cancel()
22 const startedAt = await $.clock.now()
23 await update($, label, () => 'turn 0s')
24 tick = $.clock.every(1000, async () => {
25 const elapsed = formatDuration((await $.clock.now()) - startedAt)
26 await update($, label, () => `turn ${elapsed}`)
27 })
28
29 return next(e)
30 })
31
32 on('turn.complete', async ($, e, next) => {
33 if (e.agentId === undefined) {
34 tick?.cancel()
35 tick = undefined
36 const verb = e.isAborted ? 'interrupted after' : 'last turn'
37 await update($, label, () => `${verb} ${formatDuration(e.durationMs)}`)
38 }
39
40 return next(e)
41 })
42
43 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
44 const text = await read($, label)
45 if (text === null) return next(e)
46
47 return next({ ...e, props: { ...e.props, modes: [...e.props.modes, text] } })
48 })
49}
50types/index.d.ts 11 lines1// ABOUTME: Type contract for the turn-timer mod's session state.
2// ABOUTME: Declares the label drawn in the prompt footer's mode area.
3
4export type TurnTimerLabel = string | null
5
6declare module 'claude-code' {
7 interface PluginState {
8 'turn-timer': { label: TurnTimerLabel }
9 }
10}
11