SLOPSHOPPER

context-bar

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

bandcommandprompt
v0.1.0no licenseupdated 2026-10-20mathifonseca/dotfiles/.claude/mods/context-bar
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · context-bar
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /context-bar ⎿ context-bar: Context bar off. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

dotfiles

Personal macOS configuration files. Clone this repo on a fresh Mac and run a few commands to get a fully configured development environment.

Structure

.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)

Fresh Mac Setup

1. Install Xcode Command Line Tools

xcode-select --install

2. Clone this repo

mkdir -p ~/code
git clone git@github.com:mathifonseca/dotfiles.git ~/code/.dotfiles
cd ~/code/.dotfiles

The $DOTFILES env var is resolved dynamically from the ~/.zshrc symlink, so the clone path is not hardcoded — but ~/code/.dotfiles is the canonical location.

3. Install Homebrew and Oh My Zsh

./brew_itself.sh

This installs Homebrew and Oh My Zsh.

4. Install everything

make install

This runs all steps in order:

  • Initializes git submodules (zsh-autosuggestions, zsh-syntax-highlighting)
  • Creates symlinks (.zshrc, .gitconfig, .hushlogin, Ghostty config, Starship config, Claude Code config)
  • Installs all Homebrew packages and casks
  • Applies macOS system preferences and security hardening

Or run individual steps:

make bootstrap  # just submodules + symlinks
make brew       # just Homebrew packages
make macos      # just macOS preferences
make symlinks   # just symlinks

5. Post-setup

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.json contain 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 --global will 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.

Manual Steps

Some things can't be automated:

  • Add user home folder to Finder sidebar
  • Delete unused tags in Finder
  • Set up collapsible Dock
  • Sign in to apps (1Password, Chrome, Slack, Spotify, etc.)
  • Enable FileVault if not already on (System Settings > Privacy & Security > FileVault)

What Gets Installed

Homebrew Formulae

CategoryPackages
Modern macOS toolscoreutils, moreutils, findutils, binutils, bash, zsh, wget, curl, gnu-sed, gnu-tar, gnupg, git, vim, grep, openssh, screen, gmp
System utilitiesrename, tree, pstree, mas, htop, pidof, watch
Modern CLI replacementsbat, eza, fd, ripgrep, fzf, git-delta, zoxide, jq, tlrc
Dev toolsawscli, starship, graphviz, go, gh
Fontswoff2

Modern CLI Replacements

These tools replace classic Unix commands with faster, more user-friendly alternatives:

ToolReplacesWhat it does
batcatSyntax highlighting, line numbers, git integration
ezalsIcons, colors, git status, tree view
fdfindSimpler syntax, respects .gitignore, much faster
ripgrepgrepBlazing fast recursive search, .gitignore-aware
fzf—Fuzzy finder for files, history, processes
deltadiffBeautiful syntax-highlighted git diffs
zoxidecd/zLearns your most used directories
jq—JSON processor
tlrcmanCommunity-driven simplified man pages

Aliases are configured so classic command names use the modern tools:

You typeRuns
catbat
lseza
lleza -alh --group-directories-first --icons
treeeza --tree --level=2
diffdelta

fd, rg, fzf, jq, and tldr are used by their own names. zoxide replaces the z command automatically.

Casks (GUI Apps)

CategoryApps
TerminalGhostty
BrowsersGoogle Chrome, Arc
ChatWhatsApp, Zoom, Granola, Slack
DesktopShottr, The Unarchiver, Calendr, Rectangle
Security1Password, NordVPN
Text / NotesNotion, Obsidian, Sublime Text, Zed
AIClaude (desktop), Claude Code (CLI), Wispr Flow, OpenUsage
DevZulu (JDK), Postman, SourceTree, Docker Desktop, Linear, Bruno
FontsFira Code
FunSpotify, VLC, Steam

Shell Setup

Oh My Zsh Plugins

PluginWhat it does
gitGit aliases and functions
web-searchSearch Google/DuckDuckGo from the terminal
dirhistoryNavigate directory history with keyboard shortcuts
historyHistory command shortcuts
macosmacOS-specific utilities (e.g. ofd, pfd)
brewHomebrew aliases
zsh-syntax-highlightingReal-time command syntax highlighting
zsh-autosuggestionsFish-like autosuggestions

Custom Aliases

AliasCommandDescription
gsgit statusGit status
gagit add ../..Git add from two levels up
gplgit pullGit pull
gpsgit pushGit push
ggitShort git
ssubl .Open current dir in Sublime Text
oopen .Open current dir in Finder
ipdig +short myip.opendns.com @resolver1.opendns.comShow public IP
copysshpbcopy < ~/.ssh/id_rsa.pubCopy SSH public key
zshconfigsubl ~/.zshrcEdit zsh config

Custom Functions

FunctionDescription
fuckdockerStop 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.

Key Bindings

ShortcutAction
Ctrl+RFuzzy search command history (fzf)
Ctrl+TFuzzy find files (fzf)
Option+FMove cursor forward one word
Option+BMove cursor backward one word

Prompt

Uses Starship cross-shell prompt with custom config:

  • Directory path (truncated to git root)
  • Git branch + status + lines added/removed
  • Language versions (Node, Go, Java) shown when relevant
  • Command duration for slow commands (>2s)
  • Current time on the right side

Terminal

Uses Ghostty with:

  • Chalk theme
  • Fira Code font (size 14)
  • Slight transparency with background blur
  • Non-blinking bar cursor
  • Auto-copy on select
  • Tab/split state restored on restart

Claude Code

Configuration for Claude Code, Anthropic's CLI for AI-assisted development. Files are symlinked to ~/.claude/.

FilePurpose
.claude/CLAUDE.mdGlobal instructions loaded in every Claude Code session (tool preferences, modern CLI aliases)
.claude/settings.jsonPermissions (allowed commands, MCP tools), hooks (GSD context monitor), status line config
.claude/sdlc.mdSoftware Development Lifecycle Guide (versioned, currently v1.1.0) — methodology for AI-first development
.claude/settings.local.jsonTemplate for project-level settings (deny rules for .env files) — copy to projects, not symlinked

Claude Code mods

.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.

SDLC Guide

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).

What's NOT tracked here

Claude Code generates runtime state in ~/.claude/ that is machine-specific and should not be version-controlled:

  • sessions/, history.jsonl — conversation history
  • cache/, file-history/, image-cache/, paste-cache/ — caches
  • projects/ — per-project auto-generated memory
  • dev-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 state

Git Config

Versioned at git/.gitconfig, symlinked to ~/.gitconfig:

  • delta as the pager (side-by-side, line numbers, syntax highlighting)
  • zdiff3 conflict style for clearer merge conflicts
  • SourceTree as difftool/mergetool
  • Global gitignore (.DS_Store, *~)

macOS Preferences

The macos.sh script configures system-wide preferences via defaults write. Here's what it touches:

AreaKey settings
SystemShow hidden files, disable boot sound, enable firewall + stealth mode, expand save panels, metric units, custom date format
SecurityFirewall stealth mode, AirDrop disabled, Handoff disabled, Siri disabled, crash reporter suppressed, FileVault check
TrackpadTap to click enabled
KeyboardDisable Option+Space non-breaking space
ScreenPassword required immediately after sleep
FinderNo animations, show extensions/status bar/path bar, list view, search current folder, screenshots to ~/Pictures/Screenshots
SafariPrivacy hardened: no search suggestions to Apple, no AutoFill, no plugins/Java, block popups, Do Not Track, Safe Browsing
ChromeBlock third-party cookies, disable search suggestions/telemetry/AutoFill/password manager, Safe Browsing, disable backswipe
ArcSame privacy/security settings as Chrome
TerminalUTF-8 only
Activity MonitorShow all processes, sort by CPU, CPU icon in Dock
PhotosDisable auto-open on device plug-in
MessagesDisable spell checking

Inspiration

Source 3 files
hooks/register.tsx 104 lines
1import { 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}
104
hooks/segments.ts 76 lines
1import 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}
76
types/index.d.ts 9 lines
1export 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