Cross-platform dotfiles managed with GNU Stow. Targets Arch Linux (native + WSL2), macOS, and Steam Deck (SteamOS via Nix).
The pi coding-agent harness under .pi/agent/ (66 extensions, 61 skills,
8 prompt templates, theme) is also packaged as a pi package
(@erfianugrah/pi-harness, root package.json). Install it standalone on any
machine - no stow required - with pi install git:github.com/erfianugrah/dotfiles.
See .pi/agent/README.md and the "Cross-machine install"
section of AGENTS.md.
.zshrc # main shell config (zinit, plugins, aliases, PATH)
.p10k.zsh # Powerlevel10k theme (lean 8-color, 1-line)
.tmux.conf # tmux config (TPM, tokyo-night, vim-navigator)
.wezterm.lua # WezTerm terminal (workspaces, splits, WSL)
.gitconfig # Git (GPG signing, delta pager, diff3 merge)
.vimrc # minimal vim (numbers, syntax, mouse)
.ssh/config # SSH hosts (Cloudflare Tunnel, Proxmox, TuringPi, etc.)
functions.zsh # modular loader - sources functions.d/*
functions.d/
system.zsh # OS detection, update_all, fix_file_limits
crypto.zsh # SOPS/Age encrypt & decrypt
bitwarden.zsh # bw serve API, cache, env loaders
terraform.zsh # tf_out, debug toggles, cf_permissions
misc.zsh # ansible, tmux, yazi, p10k helpers
packages.zsh # install_packages, save_packages, diff_packages
packages/
arch-repo.txt # pacman native repo packages
arch-aur.txt # AUR packages (paru)
brew.txt # Homebrew formulae (macOS)
brew-cask.txt # Homebrew casks (GUI apps)
npm-globals.txt # npm global packages
go-tools.txt # go install modules
cargo-tools.txt # cargo install crates
pip-requirements.txt # pip user packages
deno-tools.txt # deno installed tools
standalone.txt # binary downloads (~/.local/bin)
nix/
flake.nix # Home Manager flake (Steam Deck)
home.nix # declarative package list + config
bin/
caddyfmt # Caddyfile formatter (Python, stdin/stdout)
.config/
atuin/config.toml # Atuin shell history (self-hosted sync)
systemd/user/bw-serve.service # Bitwarden CLI REST API service
.claude/ # Claude Code wiring (user-level, stow-linked to ~/.claude/)
CLAUDE.md # universal agent rules (harness-agnostic subset of APPEND_SYSTEM.md)
skills/ # per-skill relative symlinks -> ../../.pi/agent/skills/<name>
# (23 promoted today - the directory IS the allowlist, see AGENTS.md
# "Agent-surface routing"; coexists with the local Cloudflare skill set.
# settings.json deliberately NOT tracked - Claude mutates it live)
.pi/agent/ # pi AI coding agent (PRIMARY harness; canonical skills + resources)
APPEND_SYSTEM.md # appended: Commit/PR, Safety, Epistemic calibration, Confidential-IDs, Output
skills/ # 61 skills, flat (canonical since 2026-05-27; superpowers tree absorbed 2026-08-16)
prompts/ # markdown sources loaded by extensions
local-model-rules.md # prepended only for gemma/qwen/llama-server models
commit.md, pr.md, review.md, test.md, init.md, rollback.md, docs-reference.md
extensions/ # TypeScript plugins
tool-routing.ts # prepends prompts/tool-routing.md (above the tool-routing:end
# marker) as system-prompt prefix; resolves self-relative to
# its own module path so pi-package checkouts work
exa.ts, webfetch.ts, oci-tags.ts, web-research.ts
docs.ts, context7.ts, session-search.ts
memory.ts, todowrite.ts, task.ts, question.ts
git-gh-gate.ts, local-model-rules.ts, lsp/
render-diagram.ts # mermaid + d2 via local mmdc / d2 CLI
build-favicon-set.ts # SVG/PNG → full PWA favicon set
themes/ # pi TUI themes (opencode-dark, etc.)
models.json, settings.json
.git-template/hooks/pre-commit # global pre-commit: block unencrypted secrets
wezterm.sh # WezTerm shell integration (OSC 7/133)
tests/
run-all.sh # Docker test matrix runner
harness.zsh # shared test assertions
test-arch.zsh # Arch Linux test (system + ecosystem)
test-steamos.zsh # Steam Deck test (nix + detection)
test-macos.zsh # macOS simulation (Linuxbrew + validation)
Dockerfile.arch # cached Arch image
Dockerfile.steamos # Arch + Nix + deck markers
Dockerfile.macos # Ubuntu + Linuxbrew
| Platform | Pkg manager | Lists |
|---|---|---|
| Arch Linux (native, WSL2) | pacman + paru (AUR) | packages/arch-repo.txt, packages/arch-aur.txt |
| macOS | Homebrew (formulae + casks) | packages/brew.txt, packages/brew-cask.txt |
| Steam Deck (SteamOS) | Nix (Home Manager + flakes) | packages/nix/flake.nix, packages/nix/home.nix |
Platform detected automatically at shell startup via _SYS_OS / _SYS_PKG
(set by system.zsh). Steam Deck identified by /etc/steamos-release or
$USER == deck.
# 1. Clone and stow
git clone git@github.com:erfianugrah/dotfiles.git ~/dotfiles
cd ~/dotfiles
stow .
# 2. Install packages for your platform
install_packages # auto-detects: brew / pacman+paru / nix
# 3. First shell launch auto-installs zinit, powerlevel10k, and pluginssudo pacman -S zsh stow git
chsh -s /usr/bin/zsh
# Install paru (AUR helper) if not present
sudo pacman -S --needed base-devel
git clone https://aur.archlinux.org/paru.git /tmp/paru && cd /tmp/paru && makepkg -si
cd ~/dotfiles && stow .
install_packages/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install zsh stow git
chsh -s /opt/homebrew/bin/zsh
cd ~/dotfiles && stow .
install_packagesNote: Terraform requires the HashiCorp tap (
hashicorp/tap/terraform). The brew list includes the full tap path;install_packageshandles this automatically.
SteamOS is immutable - pacman installs get wiped on OS updates. Nix
survives updates by storing everything in /nix/store.
# Switch to Desktop Mode, open Konsole, set password
passwd
# Install Nix (Determinate Systems installer)
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
sudo reboot
# After reboot
nix run nixpkgs#hello # verify nix works
cd ~/dotfiles && stow .
install_packages # copies flake to ~/.config/home-manager, runs home-manager switchTo add/remove packages, edit packages/nix/home.nix and run
install_packages again. To update all nix packages:
cd ~/.config/home-manager
nix flake update
home-manager switch --flake .#deckinstall_packages runs three phases in order:
| Phase | What | Lists |
|---|---|---|
| 1. System | pacman+paru / brew / nix | arch-repo.txt, arch-aur.txt, brew.txt, brew-cask.txt, nix/ |
| 2. Ecosystems | npm, go, cargo, pip, deno | npm-globals.txt, go-tools.txt, cargo-tools.txt, pip-requirements.txt, deno-tools.txt |
| 3. Standalone | Binary downloads to ~/.local/bin |
standalone.txt |
Phase 1 installs runtimes (node, go, rust, python, deno). Phase 2 needs those runtimes. Phase 3 handles tools with custom install methods.
npm globals isolation: On Arch, npm is installed via pacman and its global prefix is
/usr/lib.install_packages npmautomatically redirects globals to~/.npm-global/to avoid conflicts with pacman-managed packages onpacman -Syu. The~/.npm-global/binis in PATH via.zshrc. System npm packages (bitwarden-cli,pnpm,yarn) are always owned by the system package manager.
install_packages # all three phases
install_packages system # phase 1 only (pacman+paru / brew / nix)
install_packages ecosystem # phase 2 only (npm, go, cargo, pip, deno)
install_packages standalone # phase 3 only (binary downloads)
install_packages npm # single ecosystem
install_packages go # single ecosystemsave_packages # snapshot all installed packages to list files
save_packages system # system lists only
save_packages ecosystem # ecosystem lists only
diff_packages # show drift: + installed but not in list, - missingsave_packages captures the current state:
- Arch:
pacman -Qqen(repo) andpacman -Qqem(AUR) - macOS:
brew leaves(formulae) andbrew list --cask(casks) - Ecosystems:
npm list -g,go version -m,cargo install --list,pip list --user, deno bins - Steam Deck: packages managed declaratively in
home.nix
Tapped formulae caveat:
brew leavesoutputs short names. Tapped packages (e.g.hashicorp/tap/terraform) may save asterraform. Verify tap paths after runningsave_packages.
These tools need one-time setup beyond package installation.
IosevkaTerm Nerd Font is the configured font for WezTerm and the terminal.
- Arch:
ttf-iosevkaterm-nerd(included in package lists) - macOS: download from Nerd Fonts releases
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm
tmux source ~/.tmux.conf
# prefix (Ctrl-A) + I to install pluginsmacOS yellow status bar:
tokyo-night-tmux'sthemes.shusesdeclare -A(bash >= 4.2), but macOS ships bash 3.2 at/bin/bashand the script bails -> default yellow/orange bar. Fix:brew install bash(now inpackages/brew.txt, soinstall_packageshandles it), thentmux kill-serverand reopen - the running server caches the old PATH, so a restart is required..tmux.confalready prepends Homebrew to the tmux server PATH soenv bashfinds 4.2+.
Installed automatically by install_packages (Phase 3 standalone). Run
install_pi directly to update or to replace an npm/Node pi install.
install_pi # fetches the standalone Bun binary -> ~/.local/opt/pi + ~/.local/bin/piUse the standalone Bun binary, not
npm install -g/pi.dev/install.sh(those install the Node build, which lacksbun:sqliteand breaks Bun-only extensions likesession-ftswith "bun:sqlite module not found").
git clone https://github.com/erfianugrah/kickstart.nvim.git "${XDG_CONFIG_HOME:-$HOME/.config}"/nvimSelf-hosted sync server at atuin.erfi.io. Install the client:
curl --proto '=https' --tlsv1.2 -LsSf https://setup.atuin.sh | shConfig at .config/atuin/config.toml (stowed automatically).
curl https://sh.rustup.rs -sSf | shcurl -fsSL https://bun.sh/install | bash
curl -fsSL https://deno.land/install.sh | shGit commits are signed with GPG key B9D283E8AE4E56B4. Import your key and
ensure gpg-agent is running:
gpg --import <your-key>
export GPG_TTY=$(tty) # already in .zshrcgpg-agent.conf (tracked at .gnupg/gpg-agent.conf, stow-linked) caches the
passphrase for 7 days idle / 30 days max, so headless shells (tmux loops,
pi -p subagents) can sign without a TTY for pinentry. If a commit dies
with gpg failed to sign the data, the cache is cold - NEVER bypass with
--no-gpg-sign; warm it instead:
gpg_unlock # seeds from Vaultwarden item GPG_KEY_PASSPHRASE via bw serve
# (no TTY needed), or prompts once via pinentry interactivelyIn practice you should never need to run it: bw_serve_start seeds the
agent automatically after unlocking, and a zsh preexec hook re-seeds
silently before any git commit/tag/merge/rebase/cherry-pick/
revert/am when the TTL has lapsed. Pinentry only appears when bw serve
is locked or the item is missing.
The bw item holds the passphrase in its notes field. Signing must never be
bypassed; pi's tool-guard hard-blocks --no-gpg-sign /
-c commit.gpgsign=false.
Secrets are served via bw serve on 127.0.0.1:8087. On Linux, runs as a
systemd user service. On macOS, runs via nohup in background.
# Linux: enable the systemd service (one-time, after stow)
systemctl --user daemon-reload
systemctl --user enable bw-serve.service
# All platforms: unlock vault and start the API
bw_serve_startDaily usage:
bw_serve_start # unlock vault, start API (once after login/reboot)
load_bw # export personal secrets to env
load_wrangler_token # export Cloudflare Wrangler token
load_sops_age_keys # export SOPS Age keys
bw_serve_status # check if API is running
bw_serve_sync # pull latest from Bitwarden server
bw_serve_stop # stop API, clear session
unset_bw_vars # wipe all exported secrets from current shellload_bw auto-starts the service if not running. The session survives
terminal/tmux restarts. .zshrc runs it automatically: the first
interactive shell after login prompts for the master password once (which
also seeds the gpg-agent signing cache), later shells re-export silently.
If you skip that first prompt, run bw_serve_start manually.
Shell-startup cost. Resolving the secrets means ~20 sequential HTTP
calls to the daemon (1 sync + 17 items + 2 SOPS notes), and .zshrc used to
pay that in every interactive shell - so every new tmux window. load_bw
therefore snapshots the resolved exports to $XDG_RUNTIME_DIR/bw-env.zsh
(0600, tmpfs, wiped on logout/reboot, so secrets never touch disk), and
later shells just source it.
Measured on one idle machine, min of 5: full load 1.11s, snapshot hit 0.52s, and a shell with the Bitwarden block stubbed out entirely 0.56s. That last comparison is the useful one - a snapshot hit is indistinguishable from not doing Bitwarden at all. The absolute saving is not a fixed number: it was ~2.2s when first measured on a loaded box with cold page caches and ~0.6s once everything was warm, so treat it as "between half a second and a couple of seconds, largest exactly when the machine is busy".
On hosts without XDG_RUNTIME_DIR (macOS) the snapshot falls back to
$TMPDIR, which is real disk and not guaranteed to be cleared on logout -
the file is still 0600, but the never-touches-disk property does not hold
there. Set _BW_ENV_CACHE_TTL=0 to disable the snapshot entirely if that
matters on a given machine.
The snapshot is stamped with the bw session epoch and a write time, and is
refused unless all of these hold: the stamp matches the live session, the
file is owned by you, and it is younger than _BW_ENV_CACHE_TTL. So it is
invalidated by bw_serve_start (new session) and by bw_set /
clear_bw_cache (explicit), and expires on its own after an hour. Any miss
falls through to a normal full load, which rewrites the snapshot.
Practical consequence: a secret you rotate in the web vault can be up to
an hour stale in newly-opened shells. bw_set has no such lag (it
invalidates), and load_bw in an existing shell always forces a fresh sync.
Already-running shells and agents keep their old env either way - that is
fundamental to env vars, not a caching artifact.
Staleness gotcha: a long-running serve daemon's access token can expire
silently - /status still says "unlocked" and /sync still returns
success, but the data goes stale (observed 2026-07-29). The accessors warn
on stderr once the session is >12h old, and bw_serve_status shows the
session age. If served secrets ever look stale, the fix is bw_serve_start
(fresh unlock + restart), not bw_serve_sync.
- Plugin manager: Zinit (auto-installs on first launch)
- Theme: Powerlevel10k (lean 8-color, 1-line, nerdfont-v3)
- Plugins: fzf-tab, zsh-completions, zsh-autosuggestions, history-substring-search, fast-syntax-highlighting
- OMZ snippets: tmux, git, kubectl, kubectx, terraform, opentofu, npm, python, gh, rust, ansible, sudo, colored-man-pages, and more
| Alias | Command | Notes |
|---|---|---|
k |
kubectl |
|
t |
tofu |
OpenTofu |
tf |
terraform |
|
w |
wrangler |
Cloudflare |
v |
nvim |
|
p |
python3 |
|
c |
cargo |
Rust |
s |
sentry-cli |
|
sb |
supabase |
|
ls |
eza |
guarded - falls back if eza missing |
cat |
bat |
guarded - falls back if bat missing |
| Variable | Default | Description |
|---|---|---|
EDITOR |
nvim → vim → nano → code |
first available; bindkey -e after to keep standard keybindings (zsh auto-enables vi mode when EDITOR contains vi/nvim) |
DOCKER_BUILDKIT |
1 |
always on |
LANG / LC_ALL |
C.UTF-8 |
|
ANSIBLE_PLAYBOOK_DIR |
~/my-playbooks |
used by ansible_on/off/update |
BW_SERVE_PORT |
8087 |
Bitwarden serve port |
_BW_CACHE_TTL |
300 |
in-process _bw_get cache TTL in seconds |
_BW_ENV_CACHE_TTL |
3600 |
cross-shell secret snapshot TTL in seconds; bounds how long a web-vault edit can be stale in new shells |
_TF_CACHE_TTL |
300 |
tf_out cache TTL in seconds |
- Prefix:
Ctrl-A - Plugins (TPM): sensible, vim-tmux-navigator, tmux-yank, tokyo-night
- Splits:
-horizontal,=vertical - Copy (WSL):
yin copy-mode sends toclip.exe - Base index: 1 (windows and panes)
- Mouse: enabled
- Colorscheme: Lovelace
- Font: IosevkaTerm NF, 12pt
- Leader key:
Ctrl-A(same as tmux prefix - WezTerm handles workspaces, tmux handles sessions inside) - Workspaces:
Leader+n/pnext/prev,Leader+sfuzzy picker,Leader+ccreate new - Panes:
Leader+-split vertical,Leader+=split horizontal,Leader+h/j/k/lnavigate,Leader+mzoom,Leader+Spacerotate - Tabs:
Leader+tnew,Leader+1-8switch,Leader+wclose pane,Leader+xclose tab - WSL: auto-connects to
WSL:archlinuxdomain on Windows - Scrollback: 10,000 lines, 120fps animations
- Signing: GPG commit signing enabled (key
B9D283E8AE4E56B4) - Pager: delta with navigation
- Merge:
diff3conflict style - Pull: rebase by default
- Rerere: enabled (remembers conflict resolutions)
- Fetch: auto-prune deleted remote branches
- Push:
autoSetupRemote(push new branches without-u) - Template:
~/.git-template(includes pre-commit hook) - HTTP: 500MB post buffer, HTTP/2
Hosts organized by access method:
- Cloudflare Tunnel:
*.proxmox.erfianugrah.com,pie.erfianugrah.com,*.vyos.erfianugrah.com- usescloudflared access sshas ProxyCommand - Self-hosted services:
docs.erfi.io(port 2222),git.erfi.io(port 2223, Gitea) - Infrastructure: Proxmox, VyOS routers, TuringPi cluster (rock1-4), servarr, KVMs
- Tailscale mesh: hosts accessible via
*.manticore-diatonic.ts.net - Cloud: GCP instances, Steam Deck
Global defaults (Host *):
ServerAliveInterval 60- keepalive every 60sIdentitiesOnly yes- only offer configured keys (no agent key spray)AddKeysToAgent yes- auto-add keys on first useHashKnownHosts yes- privacy for known_hosts
Global Git hook (applied to all repos via init.templateDir). Blocks
committing unencrypted sensitive files:
- Always checked:
.env,.tfvars,.tfstate- must be SOPS/Age encrypted - Content-scanned:
.yaml,.yml,.json- flagged only if they contain secret-like patterns (password,secret,token,api_key, etc.) .sops.yamlintegration: files matchingpath_regexcreation rules are checked for SOPS encryption- Escape hatches:
touch .allow-unencrypted- skip all checks for this repo.allow-unencrypted-paths- one glob pattern per line to skip specific files
- Sync: self-hosted at
atuin.erfi.io, v2 records enabled - Enter behavior:
enter_accept = true - Up arrow: disabled (uses
Ctrl-Rfor search)
functions.zsh is a thin loader that sources modular scripts from
functions.d/ in order. Each module is self-contained. The loader resolves
its own real path (${0:A:h}), so stow symlinks work transparently.
Load order: system → crypto → bitwarden → terraform → misc →
packages. Order matters: system.zsh sets _SYS_OS/_SYS_PKG used by
all other modules.
OS-aware - detects platform and package manager at source time
(apt, dnf, pacman, zypper, brew). Also picks up Flatpak and Snap.
| Function | Description |
|---|---|
update_all / upall |
Update all detected package managers (apt, dnf, pacman + AUR, zypper, brew, flatpak, snap) |
fix_file_limits / fixfiles |
Inspect and optionally raise file descriptor limits (Linux: limits.conf + sysctl, macOS: launchctl + LaunchDaemon) |
bcrypt_hash [rounds] prompts for a password (hidden, entered twice for
confirmation) and prints its bcrypt hash, verifying the hash round-trips
before printing. Default cost factor 12; requires the bcrypt python
package. Useful for seeding htpasswd / Authelia / app credential hashes.
Encrypt and decrypt files in-place using SOPS with Age keys. Requires
SOPS_AGE_KEYS to be set (use load_sops_age_keys or load_bw).
Private keys are never exported to the environment - passed inline to sops
commands via SOPS_AGE_KEY="$key" sops ... so they exist only for the
duration of each command.
| Function | Description |
|---|---|
bcrypt_hash [rounds] |
Prompt for a password (hidden, confirmed) and print its bcrypt hash (default cost 12); verifies round-trip before printing |
encrypt <file|dir> |
Encrypt a file or all files in a directory |
decrypt <file|dir> |
Decrypt a file or all files in a directory |
encrypt_all / decrypt_all |
Operate on current directory (alias for encrypt . / decrypt .) |
encrypt_k3s_secret <file> |
Encrypt K8s Secret YAML (only data/stringData fields) |
decrypt_k3s_secret <file> |
Decrypt K8s Secret YAML |
encrypt_tf / decrypt_tf |
Encrypt/decrypt Terraform sensitive files (secrets.tfvars, terraform.tfvars, blueprint-export.yaml, *.tfstate*) |
Local REST API (bw serve) with a two-tier cache. Secret mappings defined
in _BW_SECRETS / _BW_WRANGLER_SECRETS arrays - single source of truth
for load_bw, load_wrangler_token, and unset_bw_vars.
_BW_CACHE(5 min TTL) - per-process associative array behind_bw_get. Only helps repeated lookups within one shell; a new shell always starts empty._BW_ENV_CACHE(1h TTL) - the cross-shell snapshot at$XDG_RUNTIME_DIR/bw-env.zshthat makes new tmux windows cheap. See bw-serve for the invalidation rules. Values are written with zsh${(qq)}quoting, so quotes, newlines and$(...)round-trip literally and cannot execute when sourced.
| Function | Description |
|---|---|
bw_serve_start |
Unlock vault, sync, start service (systemd on Linux, nohup on macOS), wait for API |
bw_serve_stop |
Stop service, clear session file and cache |
bw_serve_status |
Check if API is reachable, show service status/logs |
bw_serve_sync |
Sync vault from Bitwarden server, clear local cache |
clear_bw_cache |
Flush the in-process cache and drop the cross-shell snapshot |
load_bw |
Sync, export personal secrets (Cloudflare, AWS, Authentik, SOPS Age keys, etc.), then write the cross-shell snapshot |
load_wrangler_token |
Export Cloudflare Wrangler API token |
load_sops_age_keys |
Export the current SOPS Age keypair (key 2) into SOPS_AGE_KEYS |
unset_bw_vars |
Wipe all Bitwarden-loaded env vars from current shell |
Generic, project-agnostic accessor for tofu output / terraform output.
Auto-detects the IaC tool. Supports fuzzy name matching, nested key
extraction, clipboard copy, env export, fzf interactive picker, and
category-based grouping.
Output JSON is cached per-project (TTL: 5 min, configurable via
_TF_CACHE_TTL). Cache files are chmod 600 inside a 700 directory under
$XDG_RUNTIME_DIR, cleaned up on shell exit via zshexit hook.
# Browse & extract
tf_out # grouped, color-coded summary
tf_out <name> # show single output with metadata
tf_out <name> <key> # extract key from object output
tf_out <name> <key.subkey> # dot-path nested extraction
# Interactive
tf_out -i | --pick # fzf picker with preview and ctrl-y copy
# Listing & filtering
tf_out -l | --list # output names only
tf_out -s | --sensitive # sensitivity & type matrix
tf_out -f | --search <pattern> # regex search output names
tf_out -y | --type <type> # filter by value type
tf_out -n | --count # count outputs by type and sensitivity
tf_out -T | --tokens # API token outputs only
tf_out -S | --s3 # S3 credential outputs only
# Data formats
tf_out -j | --json [name] # full JSON (all or single output)
tf_out -r | --raw <name> [key] # raw value for piping (no labels)
tf_out -t | --table <name> # object as aligned key=value table
tf_out -k | --keys <name> # list keys of an object output
# Actions
tf_out -c | --copy <name> [key] # copy to clipboard (wl-copy/xclip/pbcopy)
tf_out -e | --env <name> [PREFIX] # export object keys as env vars
tf_out -d | --diff <name> # diff vs last state backup
# Cache
tf_out -F | --flush # clear cache for current projectTab completion (works with fzf-tab):
tf_out <TAB>- output names with type/sensitivitytf_out -<TAB>- flags with descriptionstf_out <name> <TAB>- object keys- Preview pane shows metadata; sensitive values redacted
| Function | Description |
|---|---|
tf_debug_on / tf_debug_off / tf_debug_toggle |
Toggle TF_LOG=debug |
cf_permissions <tf|tofu> <category> |
Query Cloudflare permission groups via console (account, zone, user, r2, roles, all) |
| Function | Description |
|---|---|
ansible_on / ansible_off / ansible_update |
Ansible playbook shortcuts ($ANSIBLE_PLAYBOOK_DIR, default: ~/my-playbooks) |
tx_switch [name] |
Create and switch to a tmux session |
yy |
Open yazi file manager; cd into its last directory on exit |
p10k_colours |
Print all 256 terminal colors |
time_now |
ISO 8601 timestamp with milliseconds (cross-platform) |
Three-phase hierarchical install: system → ecosystems → standalone. Auto-detects platform. Supports per-phase and per-ecosystem targeting.
| Function | Description |
|---|---|
install_packages [phase] |
Install packages (all, system, ecosystem, standalone, or single: npm/go/cargo/pip/deno) |
save_packages [phase] |
Snapshot installed packages to lists (all, system, ecosystem) |
diff_packages |
Show drift: installed-not-in-list (+), in-list-not-installed (-) |
Files and directories excluded from symlinking into ~:
.git- prevents~/.gitsymlink (would make~look like a repo)README.md, legacy package lists (brew_packages_list.txt,pacman_list.txt,yay_list.txt)packages/,tests/- data/test files, not dotfiles.config/nvim- managed in a separate repo
| Branch | Purpose |
|---|---|
main |
Primary config (Arch WSL2) |
deck |
Steam Deck deployment |
macos |
macOS deployment |
vyos |
VyOS router config |
All platform branches track main - divergence is handled by the
cross-platform detection in system.zsh rather than branch differences.
Docker-based test matrix covering all three platforms:
./tests/run-all.sh # run all platforms
./tests/run-all.sh arch # single platform
./tests/run-all.sh steamos macos # subset| Test | Base image | What it tests |
|---|---|---|
arch |
archlinux:latest |
Full system + ecosystem install, all binaries, crypto, security, config |
steamos |
archlinux:latest + Nix |
Steam Deck detection, nix home-manager switch, all nix packages |
macos |
ubuntu:24.04 + Linuxbrew |
Brew dispatch, tap handling, list validation, crypto, config |
Arch test caches system packages in a Docker layer - rebuilds only re-run
ecosystem installs unless packages/arch-repo.txt changes.
pi.dev (Earendil Works) is the primary harness - it owns the daily-driver
TUI, extension model, skills loader, and sessions. Claude Code is the work
harness, wired to a curated subset of the same skills tree via per-skill
symlinks (see AGENTS.md "Agent-surface routing"). opencode (the earlier
custom fork at ~/opencode) was RETIRED 2026-08-15: its .config/opencode/
tree is deleted from this repo - git history is the archive.
The tool-routing rules live once at .pi/agent/prompts/tool-routing.md
(canonical since 2026-08-15, shipped in the pi package). Skills were
relocated to be pi-canonical on 2026-05-27: the source of truth is
.pi/agent/skills/.
Result: any skill added or AGENTS rule edited applies to pi on the next launch; Claude Code gets it only via deliberate per-skill promotion.
- pi -
.pi/agent/extensions/tool-routing.tsreads~/.pi/agent/prompts/tool-routing.md(everything above thetool-routing:endmarker; resolves self-relative to its own module path so pi-package checkouts loaded in place work too) and prepends it via thebefore_agent_starthook (re-runs every user prompt, so post-compaction re-injection is automatic).
The routing rules cover: search-family reformulation loop, web research
escalation (Exa <-> research SearXNG bidirectional on 0-results, fetch ->
crawler Playwright), docs.erfi.io
pipeline (search -> summary -> read), LSP for code intel, subagent
delegation (incl. the no-further-delegation rule for leaf research
subagents), memory + session_search, bash discipline (no find, sd /
ast-grep for large edits, lockfile guards).
Harness audit 2026-05-25 disabled the superpowers methodology gates; the
superpowers tree was fully removed 2026-08-16. The six surviving skills
(writing-plans, writing-skills, subagent-driven-development,
systematic-debugging, verification-before-completion,
requesting-code-review) are now first-class top-level skills - vendored,
locally curated, no upstream sync. TDD lives in the global agent rules, not
a skill. writing-specs (added the same day) is the spec-driven-design
artifact upstream of writing-plans: EARS acceptance criteria with
requirement IDs that flow into plan tasks and loop sensors.
Scaffolding + process (the orchestrators):
| Skill | Purpose |
|---|---|
scaffold-new-project |
Triggers on "start / build / scaffold a new X" - routes to the relevant concrete-tech skills below, asks at most 3 batched questions, produces project skeleton + repo-level AGENTS.md cross-referencing user-level skills. No design doc, no plan doc - just code with conventions baked in |
software-architecture |
Backend/system design - DDD bounded contexts, interface-driven deps, REST+WS surface with correlation IDs, Postgres+Valkey persistence (with the user's signature flat-single-binary go:embed full-stack Go pattern documented), slog+Prometheus observability |
| methodology skills | writing-specs / writing-plans / writing-skills / subagent-driven-development / systematic-debugging / verification-before-completion / requesting-code-review - explicit-ask-only process skills, vendored from the superpowers cut + our own additions. See .pi/agent/README.md for the three-layer taxonomy |
sa-pov |
Solutions-Architect PoV / PoC methodology - scope + negotiate success criteria, validate each live (not from docs), solution runbook with real evidence, package for the customer |
open-ended-research |
Research METHOD for set-of-candidates questions (shopping, vendors, locations, visas) - breadth-first longlist, eliminate-don't-select, adversarial pass, per-cell provenance matrix, widen-on-pushback; curator-hunt query family + research-subagent prompt rules (no further delegation) |
self-correcting-loop |
Unattended sensor-gated agent loop - runs a fresh pi -p per iteration until computational + inferential sensors (build / test / judge / visual) pass. Governor: wall-clock budgets per sensor + per agent (process-group kill, so a hang can't stall a run), optional systemd-run cgroup limits, git checkpoint/rollback, write-scope fence, bwrap jail, model-escalation ladder. Steering: rules + guide hot-reloaded from the manifest between iterations, and --trial verdicts the harness before you spend the budget. judge --adversarial N runs N independent reviewers (any reject = fail) |
epistemics |
Answering-from-memory discipline - the provenance test, cheapest-verifier routing table per claim type, the four claim labels, calibrate-do-not-hedge, and the hold-ground-under-pushback protocol. Third leg beside verification-before-completion (own work) and validating-empirically (external runtime behaviour); companion to the epistemic-guard extension |
abuse-operations |
Anti-abuse / fraud-detection system design - risk scoring, indicator design, actor / campaign tracking, false-positive handling |
validating-empirically |
Probe load-bearing claims about EXTERNAL systems on throwaway infra before asserting them - doc-reads are not runtime verification. Sibling to epistemics (recalled specifics) and verification-before-completion (own work) |
git-worktrees |
Worktrees as the default for parallel agent work (loops, concurrent sessions) - concurrency-safety argument, .worktrees/<task> convention |
relocating-repos |
Move/rename/consolidate repos safely - pre-move entanglement survey, cross-tree reference sweep, post-move verification |
Frontend + UI:
| Skill | Purpose |
|---|---|
frontend-stack |
Astro 6 / React (tsrouter) / Next.js with biome / shadcn v4 / Tailwind v4 / zod v4 / tanstack-form+query+router - includes embedded-into-Go-binary Astro pattern for full-stack Go |
design-utilitarian |
McMaster-Carr visual + interaction ethos for ANY web UI work - info density, tables over cards, no animation tax, two-color palette, no marketing prose in product surfaces |
mermaid-d2 |
Diagram language picker + render via render_diagram tool |
favicons-and-icons |
SVG-first or ComfyUI-raster to build_favicon_set to full PWA favicon set |
Writing + docs:
| Skill | Purpose |
|---|---|
erfi-voice |
Draft replies / emails / reviews / PR comments in Erfi's voice with verifiable references |
paste-formatting |
Get Markdown prose into WYSIWYG (Gmail / Docs / Notion) or chat (Slack / Discord / Telegram) targets intact via mdclip |
quarto |
Quarto documents + projects - multi-format output (HTML / PDF / Revealjs / Typst), freeze/cache, and reveal.js deck building |
deck-screenshot |
Screenshot or contact-sheet a reveal.js / Quarto deck to view + visually verify it inside a session |
writing-structure |
Long-form structure (headings, paragraph order, thesis placement) + citation conventions (Source lines, APA 7, RFC-style [TAG]) |
lexicanum |
Authoring/restructuring docs on the lexicanum site (~/lexicanum, erfi.dev) - taxonomy frontmatter, guide vs reference, IEEE-footnote citations |
Infrastructure + deploy:
| Skill | Purpose |
|---|---|
infrastructure-stack |
Self-hosted Docker Compose stacks - bridge networks + static IPs, expose-not-ports, host-mode Caddy, PUID/PGID, cross-stack shared networks |
composer |
Self-hosted Docker Compose mgmt platform at composer.servarr.erfi.io - ~109-endpoint REST API, auth, pipeline footguns, release workflow |
docker |
Dockerfile authoring, buildx multi-arch + cache, image inspection, registry workflows, BuildKit cache mounts / secrets / SSH |
fly |
Fly.io app lifecycle - deploy, secrets, certs, machines, volumes, scale + auto-stop, .internal DNS |
terraform |
OpenTofu (preferred) / Terraform - module structure, state backends, SOPS+age secrets, terraform import + cf-terraforming for adopting existing resources |
cloudflare |
CF API + wrangler + bulk Python automation - zones / DNS / rulesets / Workers / R2 / Pages / Zero Trust |
knot-dns |
Self-hosted authoritative DNS - Knot 3.5 on Fly anycast, TSIG-keyed RFC 2136 ACME for Caddy, AXFR/IXFR primary↔secondary, the CF → Knot migration path with all the documented-IPs-are-wrong gotchas |
ci-workflows |
GitHub + Gitea Actions YAML - verified-current action pins, language setup, Docker build+push, pages deploy |
gh |
gh CLI ops: PR/issue/release lifecycle, Actions runs + cache, repo + auth, gh extensions - token-efficient --json + --jq patterns |
gh-search |
Cross-repo GitHub code/issue/PR search via gh CLI |
caddy |
Custom Caddy build + WAF stack at ~/infra/ergo/caddy-compose - xcaddy plugin set, snippet idiom, wafctl dashboard, TSIG/rfc2136 chain to Knot |
knotctl |
knotctl CLI for live DNS edits (TSIG RFC 2136 over TCP) against the merged knotea authority |
gloryhole |
Self-built DNS resolver glory-hole (Go + embedded Unbound + loopback knotd + dashboard); also authoritative NS for the zones post-cutover |
tailscale-homelab |
SSH into + operate the tailscale-routed homelab (servarr etc.) - per-host identity convention, subnet routing, the ssh servarr docker ... operator idiom |
compose-backups |
Automated backup sidecars for compose stacks (offen/docker-volume-backup) - restore drills, retention pruning to R2/MinIO |
drawbridge |
mTLS-gated route-allowlisted reverse proxy for the Docker Engine API - fronts servarr's docker.sock for composer over the tailnet |
eaves |
Read-only CLI for the NixOS edge router - DHCP leases, NAT, conntrack, nftables, vnstat, doctor suite, offline fixtures |
xikectl |
Read-only CLI for the XikeStor SKS8300 switch - VLANs, interfaces, MAC table, jumbo MTU, smoke/verify suites |
souin |
Edge HTTP cache on the Caddy stack (cache-handler + nuts) - cache misses, Cache-Status anomalies, purge/reclaim |
waf-api |
wafctl WAF management API + waf-dashboard - endpoint CRUD, event export, store-vs-deploy split, adding rule/event types end-to-end |
Database + data:
| Skill | Purpose |
|---|---|
supabase |
All Supabase products (db, auth, edge fns, storage, realtime, ssr) |
supabase-postgres-best-practices |
Postgres query/schema/index patterns from Supabase |
pg-analyser |
pg-analyser CLI - Postgres performance analyzer (formerly sbperf; advisors + SQL diagnostics + infra metrics) rendering self-contained HTML + PDF reports with windowed trends |
sbshift |
sbshift CLI - near-zero-downtime Postgres to Postgres migration via native logical replication |
Local services + AI:
| Skill | Purpose |
|---|---|
research |
Multi-engine search + Playwright crawler + OSINT (SearXNG :8888, crawler :8889, OSINT :8890) - includes the platform access walls (Reddit bypass order: PullPush -> redlib -> crawler), the API-driven locator pattern (near-empty render = grep raw HTML for wp-json//api//.json), and the SearXNG silent-empty -> Exa escalation |
comfyui |
SDXL / Illustrious / Flux image generation via llm-compose proxy |
lora-train |
LoRA fine-tuning for SDXL / Flux via kohya sd-scripts |
whisper |
WhisperX audio/video transcription (YouTube, local files) |
gocurl |
gocurl CLI - HTTP performance measurement: httptrace phase breakdown (DNS / TCP / TLS / TTFB / transfer), load testing, streaming analysis |
memledger |
Cross-client session store + search (pi + opencode + claude) beyond the 30-day local retention; ingester/prune CLI, compose stack |
Homelab media (servarr):
| Skill | Purpose |
|---|---|
arr-stack |
The *arr pipeline on servarr (radarr / sonarr / prowlarr / bazarr + sabnzbd / qbittorrent / flaresolverr), TRaSH guides, quality profiles |
jellyfin |
Media-consumer stack on servarr - Jellyfin + Jellyseerr + Navidrome, NVENC transcoding on the GTX 1070 |
discord-wipe |
Delete the user's own Discord messages (guild/DM purge, rolling retention) via the discord-wipe-go daemon on servarr |
Diagnostics:
| Skill | Purpose |
|---|---|
git-troubleshooting |
Diagnostic battery for git mv / git add / pathspec failures - gitignore-first hypothesis, the symptom → cause table, recovery patterns |
powershell |
Run/write pwsh locally or on Windows machines over SSH - Get-WinEvent / Get-CimInstance patterns, PSRemoting, the powershell tool |
Custom TypeScript plugins that register tools, gates, TUI behaviour, and background jobs. Some are direct ports of opencode fork built-ins; others are pi-only because pi's extension API supports things opencode's plugin API doesn't (mid-turn tool-call gating, custom footer rendering, sync DB access, session lifecycle hooks).
Tools (called by the LLM):
| Extension | Provides |
|---|---|
docs.ts (symlink) |
docs.erfi.io SSH tools: docs_search / read / grep / find / summary / sources |
opendata.ts |
dataset_search / dataset_fetch - official open-data portals (data.gov.sg) via crawler /dataset/*; fetch writes a FILE, rows never enter context |
bash-error-hints.ts |
Decorates bash tool results with one-line hints when stderr matches a known footgun (gitignore traps, pathspec mismatch, mv ENOENT, permission denied, Anthropic stream cutoff) - the agent sees actionable next-probe text appended to the error, no context cost when no pattern fires |
exa.ts |
websearch + codesearch via mcp.exa.ai |
webfetch.ts |
URL → markdown / text / html (5MB cap, Cloudflare retry) |
web-research.ts |
Exa + auto-fetch top results + optional SearXNG cross-check; eliminates snippet-only reasoning |
oci-tags.ts |
Docker Hub / ghcr.io / quay.io tag query (no stale registry data) |
context7.ts |
Library docs via context7.com MCP |
session-search.ts |
Full-text search across past pi sessions - FTS5 fast path, ripgrep fallback for unindexed files |
glob.ts |
**/*.ts-style file pattern lookup, mtime-sorted |
grep.ts |
Ripgrep regex content search with include glob filter |
render-diagram.ts |
mermaid + d2 render via local mmdc / d2 CLI |
build-favicon-set.ts |
SVG/PNG → favicon.ico + apple-touch + 192/512/maskable + manifest + HTML snippet |
apply-patch.ts |
Multi-file Add/Update/Delete patch envelope, atomic |
task.ts |
Subagent delegation (fresh context) |
memory.ts |
Persistent cross-session memory |
todowrite.ts |
Session todo list |
lsp/ |
LSP integration (multi-language: ts, rust, py, go, lua, clangd) |
write-stream.ts |
Chunked atomic file writes for content above the tool-call-input size ceiling (first / middle / last) |
pdf.ts |
Diagnostic-first PDF extraction: born-digital to pdftotext, scanned to tesseract OCR, plus tables + visual rasterize modes |
osint.ts |
OSINT tools (domain / IP / email / username / phone / URL / threat / CVE / harvest) via the research FastAPI service |
bench.ts |
Statistical command benchmarking via hyperfine (mean / stddev / winner) |
go-test.ts |
go test -json wrapper - failures-only triage |
hurl-test.ts |
Run a .hurl file, return only failing entries |
osv-scan.ts |
osv-scanner wrapper - one flattened line per vuln |
secret-scan.ts |
gitleaks / noseyparker wrapper - secret values truncated out of context |
video-review.ts |
Transcribe + diarize a video, overlap / metrics analysis, evidence bundle (whisper stack) |
pg-analyser.pi.ts |
Drive the pg-analyser Postgres performance analyzer as a single tool |
session-ledger/ |
Cross-session work ledger - queryable structured summaries of past sessions (ledger_search / ledger_sql) |
yank.ts |
Copy a code block from the last assistant message to the system clipboard |
Gates + safety (intercept tool calls):
| Extension | Provides |
|---|---|
tool-guard.ts |
~30 rules blocking bash + write anti-patterns (incl. docs-first chain + oversized-write -> write_stream): npm-when-bun, sed -i on source files, :latest docker images, unsigned commits, hallucinated CLIs (bun create @tanstack/router), \uXXXX escapes in bash strings, chmod 777, force-push to main, edits on .env / lockfiles / node_modules / .git internals. Also a reformulation-loop guard that blocks the 4th consecutive search-family call when no drill-in tool fired between |
git-gh-gate.ts |
Confirmation modal before mutating git/gh commands (truncates display body to avoid long-session scroll cascade) |
ascii-punctuation-guard.ts |
Blocks mojibake-prone smart punctuation (em/en dash, smart quotes, ellipsis) in write / edit / apply_patch / commit payloads - keeps committed + pasted text ASCII |
confidential-write-guard.ts |
Nudges once per repo before persisting prose to a remote-backed repo; hard-blocks user-confirmed confidential third-party identifiers |
epistemic-guard.ts |
Provenance gate for the specifics the model emits. Builds a corpus of every literal seen in tool results / user messages / system prompt, then blocks writes, commits and PR bodies carrying a version, flag, system path, deep URL, CVE, perf number or date with NO provenance this session (once per specific - verifying it silences it for good). A perf number sitting in a because-clause is marked derived and routed to "name the mechanism and a precondition" instead of "go verify the literal", because a number you reasoned to is a recalled RULE applied without checking that it applies. Annotates interactive answers with a recalled, not verified footer; no footer and a block budget under pi -p so subagent payloads and loop iterations are not corrupted or taxed. /epistemics reports state. Kill switches: PI_EPISTEMIC_GUARD_OFF=1, PI_EPISTEMIC_FOOTER_OFF=1, PI_EPISTEMIC_MAX_BLOCKS=0 |
lookup-before-ask.ts |
Nudges once per session when the assistant asks the user for a fact about their OWN infrastructure - a measurement, spec, part number, date or past decision - with no memledger_search / search_ledger / session_search call this session. Those stores are pull-only, so nothing fires them when the AGENT has the gap; the cheap alternatives are asking (spends the user's turn on something already recorded) or recalling (fabricates the specific). Any lookup call disarms it. Signals are ANDed per SENTENCE, not per message. Kill switch: PI_LOOKUP_NUDGE_OFF=1 |
entity-qualifier-nudge.ts |
Nudges when a device identifier (eth0, enp2s0f0np0, br0, nvme0n1, switch ports) is cited as EVIDENCE with no host named. eth0 is not unique across boxes, so a real fact about one interface silently becomes an argument about a different one; writing "servarr's eth0" is the moment the mismatch shows. A date does not qualify - "eth0 flapped on 2026-08-08" says when, not which box. Kill switch: PI_ENTITY_NUDGE_OFF=1 |
skill-guard.ts |
Actively routes to a matching skill the model would otherwise skip: intent nudge on the prompt (before_agent_start) + block-once on skill-relevant file edits / commands (tool_call). Companion to the passive skill-description layer |
slash-typo-guard.ts |
Catches typo'd slash commands before they reach the LLM |
cd-agents-reload.ts |
Warns when you cd into a repo whose AGENTS.md was not loaded at session start |
Prompt + policy layer:
| Extension | Provides |
|---|---|
tool-routing.ts |
Prepends prompts/tool-routing.md (above the tool-routing:end marker; legacy-path fallback) with CRITICAL framing on every user prompt |
local-model-rules.ts |
Per-model rules for gemma / qwen / llama-server |
style-toggle.ts |
Per-session output-style switcher |
Session lifecycle + UX:
| Extension | Provides |
|---|---|
atuin.ts |
Tracks pi's bash calls in Atuin history (author pi) via tool_call/tool_execution_end events |
custom-footer.ts (disabled) |
Retired to custom-footer.ts.disabled - pi's built-in footer now covers cost/tokens/context. Kept for reference: cumulative cost + per-turn delta, true input tokens (sums input + cacheRead + cacheWrite), width-aware right-side field drop, NaN-guarded accumulators (pi#4158), status aggregation |
session-auto-title.ts |
Auto-generates a 3-6 word session title from the first user message via a small cheap model. Model picker reads ~/.pi/agent/models.json, scores every configured provider/id pair by (provider weight + name pattern weight) - local llama-server / ollama / lmstudio first, then haiku / mini / nano patterns, then gemma / qwen3-4 / phi / llama-3-small patterns. First with valid auth wins. Falls back to current session model only if nothing else has auth. Records a marker so it runs once per session and respects manual /session-name overrides |
session-summary.ts |
On startup / new session_start, injects a project briefing: branch + ahead/behind, working-tree status counts, last 3 commits, up to 3 open PRs. Hard 1.5s budget; silent outside git working trees |
session-fts/ |
Background SQLite FTS5 indexer for ~/.pi/agent/sessions/. Two files: index.ts (main-thread façade - spawns worker, owns read-only DB handle for searchFts() + indexStats()) and worker.ts (Bun Worker - owns writer-side DB, runs all synchronous SQLite churn off the main event loop). On a 1.3GB index every INSERT costs ~3ms because FTS5 has to update its inverted index; 100 files × ~150 rows = ~45s of unyielding work that previously caused visible typing lag at every session_start. Worker thread eliminates that. WAL mode allows concurrent reader+writer. 100 newest-first files per startup, 5s startup delay, single-flight guard prevents stacked requests. /session-index status | rebuild | gc |
compaction-progress.ts |
Live spinner + token-before/after toast during /compact |
tool-output-prune.ts |
opencode-style surgical pruning of oversized tool outputs to preserve context |
compaction-model.ts |
Runs pi's compaction summarizer on a cheaper / faster model |
continue-after-error.ts |
Recovery affordance for provider 401 / 402 / 429 - resume instead of ending the run |
clipboard-image-shrink.ts |
Auto-downscales pasted clipboard images before they reach the model |
bookmark.ts, migrate-sessions.ts, notify.ts, question.ts, session-name.ts, session-undo.ts, trigger-compact.ts, inline-bash.ts |
Smaller utilities (/undo, bookmarks, desktop notify, prompts, session naming, threshold compaction, !{cmd} inline bash) |
Two extensions are parked as .disabled: custom-footer.ts (superseded by pi's built-in footer) and stuck-state-recovery.ts.
pi.dev is the current daily driver: a richer extension API
(before_agent_start, tool_call / tool_result gating, custom tool
rendering), larger TUI primitives (modals, widgets, status slots), a task /
subagent system, and the extension surface documented above. opencode (the
earlier fork, with its output-rules.ts plugin pattern and builtin Exa /
codesearch / context7) was retired 2026-08-15. Claude Code gets a curated
subset: symlink individual skills into
~/.claude/skills/ (whole-dir symlink would clobber the existing Cloudflare
skill set there; a tracked dotfiles/.claude/skills/<name> ->
../../.pi/agent/skills/<name> per-skill symlink stows cleanly alongside
them). A user-level ~/.claude/CLAUDE.md carries the universal rules.
Minimal Caddyfile formatter (Python 3, stdin/stdout). Replicates caddy fmt:
normalizes indentation, handles Caddy placeholders ({$ENV},
{http.request.uri}), strips trailing whitespace.
caddyfmt < Caddyfile > Caddyfile.formattedWezTerm shell integration (sourced by .zshrc). Sets up:
- OSC 7: report current working directory to terminal
- OSC 133: semantic prompt zones (command input vs output)
- User vars for WezTerm's Lua API