Draws the context window as a stacked bar above the prompt, one color per /context category; toggle with /context-bar

Personal macOS configuration files. Clone this repo on a fresh Mac and run a few commands to get a fully configured development environment.
.dotfiles/
├── Makefile # Main entry point (make install, make brew, etc.)
├── bootstrap.sh # Pulls latest changes, inits submodules, creates symlinks
├── brew_itself.sh # Installs Homebrew and Oh My Zsh
├── Brewfile # All Homebrew formulae and casks
├── macos.sh # macOS system preferences and security hardening
├── .claude/
│ ├── CLAUDE.md # Global Claude Code instructions (symlinked to ~/.claude/)
│ ├── settings.json # Claude Code permissions and hooks (symlinked to ~/.claude/)
│ ├── sdlc.md # SDLC guide v1.1.0 (symlinked to ~/.claude/)
│ ├── settings.local.json # Template for project deny rules (not symlinked)
│ ├── scripts/update-skills.sh # Pulls every ~/code/claude-* skill repo (run by launchd + alias)
│ └── launchd/ # LaunchAgents (symlinked to ~/Library/LaunchAgents/)
│ └── com.mathifonseca.claude-skills-update.plist
├── git/
│ ├── .gitconfig # Git config with delta pager (symlinked to ~)
│ └── .gitignore_global # Global gitignore (symlinked to ~)
├── ghostty/
│ └── config # Ghostty terminal config (symlinked to ~/.config/ghostty/)
├── starship/
│ └── starship.toml # Starship prompt config (symlinked to ~/.config/)
├── shell/
│ └── .hushlogin # Suppresses "Last login" message (symlinked to ~)
└── zsh/
├── .zshrc # Main shell config (symlinked to ~)
└── custom/
├── aliases.zsh # Custom aliases (auto-loaded by Oh My Zsh)
├── path.zsh # PATH modifications (auto-loaded by Oh My Zsh)
├── functions/
│ └── git_remerge # Autoloaded shell function
└── plugins/
├── zsh-autosuggestions/ # (git submodule)
└── zsh-syntax-highlighting/ # (git submodule)
xcode-select --install
mkdir -p ~/code
git clone git@github.com:mathifonseca/dotfiles.git ~/code/.dotfiles
cd ~/code/.dotfiles
The
$DOTFILESenv var is resolved dynamically from the~/.zshrcsymlink, so the clone path is not hardcoded — but~/code/.dotfilesis the canonical location.
./brew_itself.sh
This installs Homebrew and Oh My Zsh.
make install
This runs all steps in order:
.zshrc, .gitconfig, .hushlogin, Ghostty config, Starship config, Claude Code config)Or run individual steps:
make bootstrap # just submodules + symlinks
make brew # just Homebrew packages
make macos # just macOS preferences
make symlinks # just symlinks
Node.js is managed via nvm, not Homebrew. After setup:
nvm install --lts
GSD (Get Shit Done) is the workflow framework that backs every hook referenced in .claude/settings.json (gsd-context-monitor, gsd-prompt-guard, gsd-statusline, etc.). It is not installed by make install — install it separately following the instructions at mathifonseca/get-shit-done. Until it's installed, Claude Code will emit hook errors at session start.
Re-run the GSD installer on every fresh machine. The hook commands in
.claude/settings.jsoncontain absolute paths to the active nvm node binary (e.g.~/.nvm/versions/node/v24.15.0/bin/node), rewritten by the installer to work in non-interactive shells. After bootstrapping a new machine,npx get-shit-done-cc@latest --claude --globalwill rewrite those paths to match the local node version.
Claude Code mods live in .claude/mods/ (currently context-bar, the context-window bar above the prompt). They are not symlinked: they are installed as plugins from a local marketplace, and that registration lives in ~/.claude/settings.json, which is not tracked. On a fresh machine, run once:
claude plugin marketplace add ~/code/.dotfiles/.claude/mods/context-bar
claude plugin install context-bar@mathi-mods --scope user
(Adjust the path if the repo is cloned elsewhere.) See Claude Code mods.
Restart your terminal (or source ~/.zshrc) to pick up all changes.
Some things can't be automated:
| Category | Packages |
|---|---|
| Modern macOS tools | coreutils, moreutils, findutils, binutils, bash, zsh, wget, curl, gnu-sed, gnu-tar, gnupg, git, vim, grep, openssh, screen, gmp |
| System utilities | rename, tree, pstree, mas, htop, pidof, watch |
| Modern CLI replacements | bat, eza, fd, ripgrep, fzf, git-delta, zoxide, jq, tlrc |
| Dev tools | awscli, starship, graphviz, go, gh |
| Fonts | woff2 |
These tools replace classic Unix commands with faster, more user-friendly alternatives:
| Tool | Replaces | What it does |
|---|---|---|
| bat | cat | Syntax highlighting, line numbers, git integration |
| eza | ls | Icons, colors, git status, tree view |
| fd | find | Simpler syntax, respects .gitignore, much faster |
| ripgrep | grep | Blazing fast recursive search, .gitignore-aware |
| fzf | — | Fuzzy finder for files, history, processes |
| delta | diff | Beautiful syntax-highlighted git diffs |
| zoxide | cd/z | Learns your most used directories |
| jq | — | JSON processor |
| tlrc | man | Community-driven simplified man pages |
Aliases are configured so classic command names use the modern tools:
| You type | Runs |
|---|---|
cat | bat |
ls | eza |
ll | eza -alh --group-directories-first --icons |
tree | eza --tree --level=2 |
diff | delta |
fd, rg, fzf, jq, and tldr are used by their own names. zoxide replaces the z command automatically.
| Category | Apps |
|---|---|
| Terminal | Ghostty |
| Browsers | Google Chrome, Arc |
| Chat | WhatsApp, Zoom, Granola, Slack |
| Desktop | Shottr, The Unarchiver, Calendr, Rectangle |
| Security | 1Password, NordVPN |
| Text / Notes | Notion, Obsidian, Sublime Text, Zed |
| AI | Claude (desktop), Claude Code (CLI), Wispr Flow, OpenUsage |
| Dev | Zulu (JDK), Postman, SourceTree, Docker Desktop, Linear, Bruno |
| Fonts | Fira Code |
| Fun | Spotify, VLC, Steam |
| Plugin | What it does |
|---|---|
| git | Git aliases and functions |
| web-search | Search Google/DuckDuckGo from the terminal |
| dirhistory | Navigate directory history with keyboard shortcuts |
| history | History command shortcuts |
| macos | macOS-specific utilities (e.g. ofd, pfd) |
| brew | Homebrew aliases |
| zsh-syntax-highlighting | Real-time command syntax highlighting |
| zsh-autosuggestions | Fish-like autosuggestions |
| Alias | Command | Description |
|---|---|---|
gs | git status | Git status |
ga | git add ../.. | Git add from two levels up |
gpl | git pull | Git pull |
gps | git push | Git push |
g | git | Short git |
s | subl . | Open current dir in Sublime Text |
o | open . | Open current dir in Finder |
ip | dig +short myip.opendns.com @resolver1.opendns.com | Show public IP |
copyssh | pbcopy < ~/.ssh/id_rsa.pub | Copy SSH public key |
zshconfig | subl ~/.zshrc | Edit zsh config |
| Function | Description |
|---|---|
fuckdocker | Stop and remove all Docker containers and images |
git_remerge [branch] | Checkout main (or given branch), pull latest, switch back, and merge. Handy for keeping feature branches up to date. |
| Shortcut | Action |
|---|---|
Ctrl+R | Fuzzy search command history (fzf) |
Ctrl+T | Fuzzy find files (fzf) |
Option+F | Move cursor forward one word |
Option+B | Move cursor backward one word |
Uses Starship cross-shell prompt with custom config:
Uses Ghostty with:
Configuration for Claude Code, Anthropic's CLI for AI-assisted development. Files are symlinked to ~/.claude/.
| File | Purpose |
|---|---|
.claude/CLAUDE.md | Global instructions loaded in every Claude Code session (tool preferences, modern CLI aliases) |
.claude/settings.json | Permissions (allowed commands, MCP tools), hooks (GSD context monitor), status line config |
.claude/sdlc.md | Software Development Lifecycle Guide (versioned, currently v1.1.0) — methodology for AI-first development |
.claude/settings.local.json | Template for project-level settings (deny rules for .env files) — copy to projects, not symlinked |
.claude/mods/context-bar/ draws the context window as a stacked bar above the prompt, one color per /context category, with a legend. Toggle it with /context-bar.
It is a plugin, so it only works once installed (see the post-setup step above). To change it, edit the files in the repo and run /reload-plugins in Claude Code; no reinstall needed. The marketplace is named mathi-mods; add further mods under .claude/mods/ and list them in that folder's marketplace.json.
The sdlc.md file is a portable, versioned methodology covering 20 topics: change workflow, git hygiene, planning, testing (including frontend performance optimization), documentation, code quality, design principles, observability, database migrations, dependency management, demo-readiness, agent-ready API design, CI/CD, and AI collaboration patterns.
Distilled from building a production payments platform (34 phases, 5 milestones, 2,793+ tests) with Claude Code.
Using it in a project: Reference the version in the project's CLAUDE.md:
## SDLC
This project follows the [SDLC guide v1.1.0](~/.claude/sdlc.md).
Claude Code generates runtime state in ~/.claude/ that is machine-specific and should not be version-controlled:
sessions/, history.jsonl — conversation historycache/, file-history/, image-cache/, paste-cache/ — cachesprojects/ — per-project auto-generated memorydev-mods/ — scratch folder where mods are written before being moved to .claude/mods/get-shit-done/, agents/, hooks/, plugins/ — installed tools (managed by GSD's gsd:update)telemetry/, tasks/, plans/, backups/ — runtime stateVersioned at git/.gitconfig, symlinked to ~/.gitconfig:
zdiff3 conflict style for clearer merge conflicts.DS_Store, *~)The macos.sh script configures system-wide preferences via defaults write. Here's what it touches:
| Area | Key settings |
|---|---|
| System | Show hidden files, disable boot sound, enable firewall + stealth mode, expand save panels, metric units, custom date format |
| Security | Firewall stealth mode, AirDrop disabled, Handoff disabled, Siri disabled, crash reporter suppressed, FileVault check |
| Trackpad | Tap to click enabled |
| Keyboard | Disable Option+Space non-breaking space |
| Screen | Password required immediately after sleep |
| Finder | No animations, show extensions/status bar/path bar, list view, search current folder, screenshots to ~/Pictures/Screenshots |
| Safari | Privacy hardened: no search suggestions to Apple, no AutoFill, no plugins/Java, block popups, Do Not Track, Safe Browsing |
| Chrome | Block third-party cookies, disable search suggestions/telemetry/AutoFill/password manager, Safe Browsing, disable backswipe |
| Arc | Same privacy/security settings as Chrome |
| Terminal | UTF-8 only |
| Activity Monitor | Show all processes, sort by CPU, CPU icon in Dock |
| Photos | Disable auto-open on device plug-in |
| Messages | Disable spell checking |
hooks/register.tsx 104 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { BarData } from '../types'
5import { compact, pctColor, segments, shortModel, shortName } from './segments'
6
7const data = atom({ plugin: 'context-bar', key: 'data' } as const, null)
8const effort = atom({ plugin: 'context-bar', key: 'effort' } as const, null)
9const model = atom({ plugin: 'context-bar', key: 'model' } as const, null)
10const isShown = atom({ plugin: 'context-bar', key: 'isShown' } as const, true)
11
12async function refresh($: EngineInterface) {
13 try {
14 const { context } = await $.session.usage({ breakdown: 'summary' })
15 const b = context.breakdown
16 if (!b) return
17 const next: BarData = {
18 rows: b.categories.map(c => ({ name: c.name, tokens: c.tokens, color: c.color, kind: c.kind })),
19 max: b.rawMaxTokens,
20 total: b.totalTokens,
21 percentage: b.percentage,
22 }
23 await update($, data, () => next)
24 } catch {
25 // no session bound yet; the next event retries
26 }
27}
28
29export const register: Register = on => {
30 on('session.start', async ($, e, next) => {
31 await $.command.register({
32 name: 'context-bar',
33 description: 'Toggle the context window bar above the prompt',
34 })
35 void refresh($)
36 return next(e)
37 })
38
39 on('command.run', { command: 'context-bar' }, async $ => {
40 const now = !(await read($, isShown))
41 await update($, isShown, () => now)
42 if (now) await refresh($)
43 return { text: `Context bar ${now ? 'on' : 'off'}.` }
44 })
45
46 on('prompt.submit', async ($, e, next) => {
47 const out = await next(e)
48 void refresh($)
49 return out
50 })
51
52 // The model and effort of the main loop's latest request; it is not readable anywhere else.
53 on('turn.step', async function* ($, e, next) {
54 if (e.agentId === undefined) {
55 const level = e.effort === undefined ? null : String(e.effort)
56 await update($, effort, () => level)
57 await update($, model, () => e.model)
58 }
59 return yield* next(e)
60 })
61
62 on('turn.complete', async ($, e, next) => {
63 await refresh($)
64 return next(e)
65 })
66
67 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
68 const d = await read($, data)
69 if (e.props.hasSurvey || d === null || !(await read($, isShown))) return next(e)
70
71 const { Box, Text } = $.ui.resolve(e)
72 const level = await read($, effort)
73 const stepModel = await read($, model)
74 const tag = `${[shortModel(stepModel || d.model || ''), level].filter(Boolean).join(' · ')}`
75 const label = `${Math.round(d.percentage)}% `
76 const width = Math.max(10, (e.viewport?.columns ?? 80) - 8 - label.length)
77 const segs = segments(d.rows, d.max, width)
78
79 return (
80 <Box flexDirection="column">
81 <Text wrap="truncate">
82 <Text bold color={pctColor(d.percentage)}>{label}</Text>
83 {segs.map(s => (
84 <Text key={s.name} color={s.color}>
85 {(s.kind === 'free' ? '░' : s.kind === 'buffer' ? '▒' : '█').repeat(s.cells)}
86 </Text>
87 ))}
88 </Text>
89 <Text wrap="truncate">
90 {segs
91 .filter(s => s.kind !== 'buffer')
92 .map(s => (
93 <Text key={s.name} color={s.color}>
94 {'■ '}
95 <Text dimColor>{`${shortName(s.name)} ${compact(s.tokens)} `}</Text>
96 </Text>
97 ))}
98 </Text>
99 {tag !== '' && <Text dimColor>{tag}</Text>}
100 </Box>
101 )
102 })
103}
104hooks/segments.ts 76 lines1import type { BarRow } from '../types'
2
3export type Segment = { name: string; tokens: number; color: string; kind: BarRow['kind']; cells: number }
4
5const BY_NAME: [RegExp, string][] = [
6 [/system prompt/i, '#818cf8'],
7 [/system tools/i, '#38bdf8'],
8 [/mcp/i, '#2dd4bf'],
9 [/agent/i, '#a78bfa'],
10 [/memory/i, '#fbbf24'],
11 [/skill/i, '#fb923c'],
12 [/message/i, '#f472b6'],
13]
14const SPARE = ['#a3e635', '#f87171', '#e879f9', '#22d3ee']
15
16function paint(rows: readonly BarRow[]): BarRow[] {
17 let spare = 0
18 return rows.map(r => {
19 if (r.kind === 'free') return { ...r, color: '#6b7280' }
20 if (r.kind === 'buffer') return { ...r, color: '#9ca3af' }
21 const hit = BY_NAME.find(([re]) => re.test(r.name))
22 return { ...r, color: hit ? hit[1] : (SPARE[spare++ % SPARE.length] ?? '#a3e635') }
23 })
24}
25
26// Largest-remainder split of `width` cells across the rows, so the bar always fills exactly.
27export function segments(rows: readonly BarRow[], max: number, width: number): Segment[] {
28 const live = paint(rows).filter(r => r.kind !== 'deferred' && r.tokens > 0)
29 if (max <= 0 || width <= 0 || live.length === 0) return []
30
31 const exact = live.map(r => (r.tokens / max) * width)
32 const cells = exact.map(Math.floor)
33 let left = width - cells.reduce((a, b) => a + b, 0)
34 const order = exact
35 .map((x, i) => ({ i, frac: x - Math.floor(x) }))
36 .sort((a, b) => b.frac - a.frac)
37 for (const { i } of order) {
38 if (left <= 0) break
39 cells[i] = (cells[i] ?? 0) + 1
40 left -= 1
41 }
42
43 return live.map((r, i) => ({ ...r, cells: cells[i] ?? 0 })).filter(s => s.cells > 0)
44}
45
46export function compact(n: number): string {
47 return n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k` : String(n)
48}
49
50// ANSI colors follow the terminal theme like the statusline; `warning` is the theme's orange step.
51export function pctColor(percentage: number): string {
52 if (percentage >= 90) return 'red'
53 if (percentage >= 75) return 'warning'
54 if (percentage >= 50) return 'yellow'
55 return 'green'
56}
57
58export function shortModel(model: string): string {
59 return model.replace(/^claude-/, '')
60}
61
62const SHORT: [RegExp, string][] = [
63 [/system prompt/i, 'Prompt'],
64 [/system tools/i, 'Tools'],
65 [/mcp/i, 'MCP'],
66 [/custom agents|agents/i, 'Agents'],
67 [/memory/i, 'Memory'],
68 [/skills/i, 'Skills'],
69 [/messages/i, 'Msgs'],
70 [/free/i, 'Free'],
71]
72
73export function shortName(name: string): string {
74 return SHORT.find(([re]) => re.test(name))?.[1] ?? name
75}
76types/index.d.ts 9 lines1export type BarRow = { name: string; tokens: number; color: string; kind: 'used' | 'free' | 'buffer' | 'deferred' }
2export type BarData = { rows: BarRow[]; max: number; total: number; percentage: number; model: string }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'context-bar': { data: BarData | null; isShown: boolean; effort: string | null; model: string | null }
7 }
8}
9