Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Repository scope

Personal dotfiles spanning Windows (PowerShell 7) and Linux/WSL (bash, Neovim, tmux, i3/bspwm). Active development is on the **Windows side**: the PowerShell profile under `powershell/`, the prompt in `powershell/Profile/Set-Prompt.ps1`, the Neovim Lua config under `nvim/`, the Zellij config under `zellij/`, the Yazi config under `yazi/`, and the git config under `git/`. Configs are organized into per-tool directories: `git/`, `nvim/`, `vim/`, `bash/`, `powershell/`, `tig/`, `tmux/`, `zellij/`, `psmux/`, `yazi/`, `fzf/`, `curl/`, `claude/`, `codex/`, `windowsterminal/`, `lazygit/`, `bat/`, `vscode/`, `winget/`, `ripgrep/`, `eza/`. The Linux setup (`config/bspwm`, `config/sxhkd`, `i3/`, `tmux/`, `bash/`, `vim/`, etc.) is a legacy snapshot — touch only when explicitly asked.
Personal dotfiles spanning Windows (PowerShell 7) and Linux/WSL (bash, Neovim, tmux, i3/bspwm). Active development is on the **Windows side**: the PowerShell profile under `powershell/`, the prompt in `powershell/Profile/Set-Prompt.ps1`, the Neovim Lua config under `nvim/`, the Zellij config under `zellij/`, the Yazi config under `yazi/`, and the git config under `git/`. Configs are organized into per-tool directories: `git/`, `nvim/`, `vim/`, `bash/`, `powershell/`, `tig/`, `tmux/`, `zellij/`, `psmux/`, `herdr/`, `yazi/`, `fzf/`, `curl/`, `claude/`, `codex/`, `windowsterminal/`, `lazygit/`, `bat/`, `vscode/`, `winget/`, `ripgrep/`, `eza/`. The Linux setup (`config/bspwm`, `config/sxhkd`, `i3/`, `tmux/`, `bash/`, `vim/`, etc.) is a legacy snapshot — touch only when explicitly asked.

Note the two `CLAUDE.md` files: this root one is **project instructions for the dotfiles repo**; `claude/CLAUDE.md` is the **global user instructions** that `setup.ps1 -Module claude` installs to `~/.claude/CLAUDE.md`. They are unrelated — don't merge them. See `claude/README.md` for the Claude Code module (settings, statusline, skills, agents); its files are **symlinked** into `~/.claude` on both Windows and Linux (Windows file symlinks need Developer Mode; skill dirs are junctioned on Windows), so the live files and the repo never drift.

Expand All @@ -21,8 +21,8 @@ The PowerShell profile (`powershell/Microsoft.PowerShell_profile.ps1`) is a sing

| Script | Target | Notes |
|---|---|---|
| `setup.ps1` | Windows | Module-based installer. `-Module neovim,vim,powershell,git,bash,tig,tmux,zellij,psmux,yazi,curl,claude,codex,serena,context7,fastmail,lazygit,windowsterminal,bat,vscode,winget` or `-Module all`. Supports `-DryRun`. |
| `setup.sh` | Linux / WSL | Module-based installer. `-m neovim,vim,powershell,git,bash,tig,tmux,zellij,curl,claude,lazygit,windowsterminal` or `-m all`. Supports `--dry-run`. (No `codex` module yet — Windows only.) |
| `setup.ps1` | Windows | Module-based installer. `-Module neovim,vim,powershell,git,bash,tig,tmux,zellij,psmux,herdr,yazi,curl,claude,codex,serena,context7,fastmail,lazygit,windowsterminal,bat,vscode,winget` or `-Module all`. Supports `-DryRun`. |
| `setup.sh` | Linux / WSL | Module-based installer. `-m neovim,vim,powershell,git,bash,tig,tmux,zellij,herdr,curl,claude,lazygit,windowsterminal` or `-m all`. Supports `--dry-run`. (No `codex` module yet — Windows only.) |

`setup.ps1` argument handling has Pester tests in `tests/setup.Tests.ps1` (run `Invoke-Pester -Path tests`); they invoke the installer in `-DryRun` so nothing is mutated.

Expand Down Expand Up @@ -200,6 +200,18 @@ Single config file `psmux/psmux.conf`, symlinked to `~/.psmux.conf` via `setup.p
- **Session fzf helpers** (`Enter-/Remove-/Get-PsmuxSession` + native tab-completer) live in the PowerShell profile **Phase 2b**, beside the zellij/az completers. The three functions must be declared `function global:` — a bare `function` inside the Phase-2b `Initialize-DeferredProfileSecondary` is scoped local and vanishes when it returns (the tab-completer survives only because `Register-ArgumentCompleter` is a global side-effect).
- **`tests/psmux.Tests.ps1`**: (1) a **config parse test** — loads `psmux.conf` via `psmux -f` on a private socket + throwaway HOME and asserts no parse warning (guards the trailing-comment class of bug; skips when psmux is absent, as CI has none); and (2) a **`save.ps1` zero-session guard test** — drives `save.ps1` with a PATH-shim fake `psmux` (empty-output and stderr-error cases) and asserts the previous `last` snapshot is kept and the guard's skip message + exit code fire (so it can't false-green). The save test needs no psmux install, so it runs in CI.

## Herdr (`herdr/`)

[Herdr](https://herdr.dev) is a terminal workspace manager for AI coding agents — it tracks each pane's live agent session. Single config file `herdr/config.toml`, installed via `setup.ps1 -Module herdr` / `setup.sh -m herdr`. See `herdr/README.md`.

### Key decisions (do not reverse without asking)

- **Config is at `~/.config/herdr/config.toml` on every platform — including Windows** (verified from herdr's own server-log path output; it does **not** use `%APPDATA%`). Only `config.toml` is symlinked; the rest of that directory is herdr runtime state (`session.json`, `herdr.sock`, `*.log`) and must stay untracked — so the file is linked, not the whole dir (unlike Zellij/Yazi).
- **Agent integrations are (re)generated by setup, not vendored.** `Install-Herdr` runs `herdr integration install <agent>` for each agent on PATH (`claude`, `codex`). It is **idempotent** (a re-run is a verified no-op) and writes files herdr manages itself — the hook script (`~/.claude/hooks/herdr-agent-state.ps1`, `~/.codex/herdr-agent-state.ps1`), `~/.codex/hooks.json`, and the hook's registration in the agent's settings. Nothing is committed to the repo because the generated blocks carry machine-absolute paths; regenerating per machine keeps them correct. This mirrors the codex module's `claude mcp add` pattern.
- **`herdr` must run AFTER `claude`/`codex` — the `-Module all` array orders it last.** `claude/settings.json` is **symlinked**, so `herdr integration install claude` writes its SessionStart block straight through into the repo file (the same self-mutate path Claude Code's own settings-writes use) — **but only once the symlink exists**. If herdr ran first on a fresh machine, that write would create a *real* `settings.json`, then the claude module's `New-FileSymlink` would back it up and replace it, silently dropping the block. `codex` differs: its registration lives in a separate `~/.codex/hooks.json` the codex module never touches, plus `[features] hooks = true` — and because `codex/config.toml` is **copied**, that flag is tracked in the repo copy so a copy-overwrite can't drop it. So codex survives any order; **claude survives only if herdr is last** (hence the array order, and the caveat: installing `claude` *after* `herdr` needs a `-Module herdr` re-run). Consequence: each `-Module herdr` run rewrites the block into the tracked `settings.json`, i.e. expected uncommitted drift — consistent with the self-mutate behaviour, and why the block is not itself committed.
- **`pi` integration is Windows-unsupported** (`herdr integration install pi` → "not supported on Windows"), so `setup.ps1` omits it; `setup.sh` includes `pi` in the wired agents for Linux/WSL.
- **`config.toml` tracks real prefs** (`onboarding=false`, `[experimental] pane_history`, `[ui]`/`[ui.toast]`, `[theme]` catppuccin) plus `[terminal] default_shell = "pwsh"`. Dry-run coverage is in `tests/setup.Tests.ps1` (asserts the config symlink line always, and the integration dry-run line when herdr + an agent are present).

## Yazi (`yazi/`)

Three config files: `yazi.toml` (manager/opener/preview settings), `keymap.toml` (keybinding additions via `prepend_keymap`), `theme.toml` (flavor reference). Installed via `setup.ps1 -Module yazi` (Windows — junction of the whole `yazi/` dir to `%AppData%\yazi\config\`).
Expand Down
8 changes: 8 additions & 0 deletions codex/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@ sandbox_mode = "workspace-write"
# (sourced from dotfiles claude/AGENTS.md). Cap the instruction chain generously so the
# full conventions file is always read.
project_doc_max_bytes = 32768

# Enable Codex's hook system so herdr's agent-state integration can report session
# activity to a running herdr server. `setup.ps1 -Module herdr` runs `herdr integration
# install codex`, which writes the hook script + ~/.codex/hooks.json; this flag turns the
# hook system on. It lives here (not left to the integration) because this file is COPIED,
# not symlinked — so a fresh -Module codex run after herdr would otherwise drop it.
[features]
hooks = true
54 changes: 54 additions & 0 deletions herdr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# herdr

[Herdr](https://herdr.dev) is a terminal workspace manager for AI coding agents. It
tracks each pane's live agent session so you can see, at a glance, which agent is running
where. This module installs the shared `config.toml` and wires herdr's agent-state hooks
into the AI agents you have installed.

## Install

```powershell
setup.ps1 -Module herdr # Windows
```
```bash
./setup.sh -m herdr # Linux / WSL
```

herdr itself is installed separately (`https://herdr.dev`); the module warns if it is not
on PATH.

## What the module does

1. **Symlinks `config.toml`** to `~/.config/herdr/config.toml` (herdr uses `~/.config` on
every platform, including Windows — it does **not** use `%APPDATA%`). Only this file is
linked; herdr keeps its runtime state (`session.json`, `herdr.sock`, `*.log`) in the same
directory and that must stay untracked.
2. **Wires agent integrations** by running `herdr integration install <agent>` for each AI
agent found on PATH:
- **claude** and **codex** on Windows and Linux.
- **pi** on Linux/WSL only — `herdr integration install pi` reports "not supported on
Windows", so `setup.ps1` omits it.

These installs are **idempotent** — re-running is a no-op. herdr owns the files they
generate (the `herdr-agent-state.ps1` hook script, `~/.codex/hooks.json`, and the hook's
registration in each agent's settings), which is why they are not committed to this repo:
the generated blocks carry machine-absolute paths, so regenerating them per machine keeps
them correct. `codex/config.toml` carries `[features] hooks = true` (needed to turn on
Codex's hook system) because that file is copied, not symlinked.

## config.toml

Only deliberate overrides live in `config.toml`; everything else stays on herdr's defaults.
Run `herdr --default-config` for the full annotated template. After editing:

```
herdr config check # validate
herdr server reload-config # apply to a running server
```

## Verify

```
herdr integration status # per-agent hook version + path
herdr config check
```
31 changes: 31 additions & 0 deletions herdr/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# herdr configuration — see `herdr --default-config` for the full annotated template.
# Only deliberate overrides live here; everything else stays on herdr's defaults.
#
# Installed by `setup.ps1 -Module herdr` / `setup.sh -m herdr` as a symlink to
# ~/.config/herdr/config.toml (herdr uses ~/.config on Windows too, not %APPDATA%).
# Only this file is linked — the rest of that directory is herdr runtime state
# (session.json, sockets, logs) and must not be tracked.
#
# Validate with `herdr config check`; apply to a running server with
# `herdr server reload-config`.

# Skip the first-run onboarding flow.
onboarding = false

[terminal]
# Resolved from PATH. Herdr otherwise falls back to $SHELL, then /bin/sh.
default_shell = "pwsh"

[experimental]
# Keep per-pane scrollback history across sessions.
pane_history = true

[ui]
show_agent_labels_on_pane_borders = true

[ui.toast]
delivery = "herdr"

[theme]
name = "catppuccin"
auto_switch = false
65 changes: 63 additions & 2 deletions setup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
before being replaced.

.PARAMETER Module
One or more modules to install: neovim, vim, powershell, git, bash, tig, tmux, zellij, psmux, yazi, curl, claude, codex, serena, context7, fastmail, lazygit, windowsterminal, bat, vscode, winget, all.
One or more modules to install: neovim, vim, powershell, git, bash, tig, tmux, zellij, psmux, herdr, yazi, curl, claude, codex, serena, context7, fastmail, lazygit, windowsterminal, bat, vscode, winget, all.
Optional when -CleanBackups is specified.

.PARAMETER DryRun
Expand Down Expand Up @@ -66,7 +66,13 @@ if (-not $env:USERPROFILE) { $env:USERPROFILE = $HOME }

# Expand 'all'
if ($Module -contains 'all') {
$Module = @('neovim', 'vim', 'powershell', 'git', 'bash', 'tig', 'tmux', 'zellij', 'psmux', 'yazi', 'curl', 'claude', 'codex', 'serena', 'context7', 'fastmail', 'lazygit', 'windowsterminal', 'bat', 'vscode', 'winget')
# 'herdr' MUST come after 'claude' and 'codex': it runs `herdr integration install <agent>`,
# which writes a hook registration into each agent's settings. claude's registration lands in
# ~/.claude/settings.json — a file the claude module SYMLINKS to the repo. If herdr ran first,
# that write would create a real settings.json, then the claude module's symlink would back it
# up and replace it, silently dropping the herdr block. Running herdr last means it writes
# through the already-established symlink into the repo file, so the block survives.
$Module = @('neovim', 'vim', 'powershell', 'git', 'bash', 'tig', 'tmux', 'zellij', 'psmux', 'yazi', 'curl', 'claude', 'codex', 'serena', 'context7', 'fastmail', 'lazygit', 'windowsterminal', 'bat', 'vscode', 'winget', 'herdr')
}
# @() wrapper: `Select-Object -Unique` over an empty array yields $null (and a
# single value yields a scalar), so without it the `$Module.Count` guard below
Expand Down Expand Up @@ -541,6 +547,60 @@ function Install-Psmux {
}
}

function Install-Herdr {
Write-Host ''
Write-Info '=== Herdr (terminal workspace manager for AI agents) ==='
# Herdr reads ~/.config/herdr/config.toml on every platform including Windows
# (verified from the server log's own path output) — it does not use %APPDATA%.
# Only config.toml is linked, NOT the whole directory: herdr keeps its runtime
# state (session.json, herdr.sock, *.log) alongside it, which must stay untracked.
$herdrConfig = Join-Path $env:USERPROFILE '.config\herdr'
# -Backup is a reverse-sync (live -> repo) and must never mutate the live side;
# New-FileSymlink skips under -Backup, so guard the mkdir the same way (mirrors Install-Zellij).
if (-not (Test-Path $herdrConfig) -and -not $Backup) {
if ($DryRun) {
Write-Info "[DRY RUN] would create: $herdrConfig"
} else {
New-Item -ItemType Directory -Path $herdrConfig -Force | Out-Null
Write-Info "Created: $herdrConfig"
}
}
$params = @{
Link = Join-Path $herdrConfig 'config.toml'
Target = Join-Path $Dotfiles 'herdr\config.toml'
}
New-FileSymlink @params

if (-not (Get-Command -Name herdr -ErrorAction Ignore)) {
Write-Warn 'herdr not found on PATH. Install from https://herdr.dev'
} else {
Write-Ok 'herdr is installed.'
Write-Info 'Validate with: herdr config check Apply live with: herdr server reload-config'

# Wire herdr's agent-state hooks into the installed AI agents so herdr can track each
# pane's live agent session. `herdr integration install <agent>` is idempotent (a re-run
# is a verified no-op) and writes agent-side files herdr manages itself — the hook script
# (~/.claude/hooks, ~/.codex) plus its registration in that agent's settings — so nothing
# is vendored here; setup regenerates it per machine with correct paths. Only agents
# actually on PATH are wired. pi is omitted: `herdr integration install pi` reports "not
# supported on Windows" (it is wired on Linux by setup.sh instead).
foreach ($agent in @('claude', 'codex')) {
if (-not (Get-Command -Name $agent -ErrorAction Ignore)) { continue }
if ($Backup) { continue } # reverse-sync must never mutate the live side
if ($DryRun) {
Write-Info "[DRY RUN] herdr integration install $agent"
continue
}
herdr integration install $agent *>$null
if ($LASTEXITCODE -eq 0) {
Write-Ok "Integration: $agent wired to herdr"
} else {
Write-Warn "herdr integration install $agent failed (exit $LASTEXITCODE)"
}
}
}
}

function Install-Yazi {
Write-Host ''
Write-Info '=== Yazi ==='
Expand Down Expand Up @@ -1114,6 +1174,7 @@ foreach ($m in $Module) {
'tmux' { Install-Tmux }
'zellij' { Install-Zellij }
'psmux' { Install-Psmux }
'herdr' { Install-Herdr }
'yazi' { Install-Yazi }
'curl' { Install-Curl }
'winget' { Install-Winget }
Expand Down
Loading