Practical guides for AI coding agents, terminal customization, and development tooling. Each guide documents a real problem encountered during daily work, the solution that fixed it, and copy-paste configurations to replicate the setup.
| Guide | Problem Solved | Time to Set Up |
|---|---|---|
| Destructive Git Command Protection | AI agent ran git checkout -- and destroyed uncommitted work |
5 min |
| Post-Compact AGENTS.md Reminder | Claude forgets project conventions after context compaction | 2 min |
| Host-Aware Terminal Colors | Can't tell which terminal is connected to production | 5-15 min |
| WezTerm Persistent Sessions | Remote terminal sessions die when Mac sleeps or reboots | 20 min |
| WezTerm Mux Tuning for Agent Swarms | Mux server becomes unresponsive with 20+ agents | 5 min |
| Safe Auto Swap Flushing | Host stays sluggish after a memory spike because cold pages are stuck in disk swap | 1 min |
| Ghostty Terminfo for Remote Machines | Numpad Enter shows [57414u garbage when SSH'd |
2 min |
| macOS NFS Auto-Mount | Have to manually mount remote dev server after every reboot | 10 min |
| Budget 10GbE Direct Link | File transfers crawl at 100MB/s through gigabit switch | 30 min |
| MX Master Tab Switching | Thumbwheel does horizontal scroll instead of something useful | 10 min |
| Doodlestein Punk Theme | Need a cyberpunk color scheme for Ghostty | 1 min |
| Reducing Vercel Build Credits | Automatic deployments burn through Pro plan credits | 10 min |
| Claude Code Native Install Fix | claude --version shows old version after native install |
5 min |
| Claude Code MCP Config Fix | MCP servers wiped out, need quick restore | 2 min |
| Mirror Claude Code Skills | Copy project skills to global ~/.claude/skills | 2 min |
| Beads Setup | Worktree errors when syncing Beads | 5 min |
| Moonlight Streaming | Remote desktop to Linux workstation with AV1 encoding | 30 min |
| Vault HA Cluster | Single Vault instance is a single point of failure | 45 min |
| DevOps CLI Tools | Clicking through web dashboards wastes time | 15 min |
| Gemini CLI Crash + Retry Fix | Gemini CLI crashes with EBADF and gives up after 10 retries (supports v0.35.x and v0.36.0+ bundle builds) | 10 sec |
| Zellij Scroll Wheel Fix | Mouse wheel triggers atuin instead of scrollback in Zellij over SSH | 10 min |
| Encrypted GitHub Issues | Need to receive sensitive security reports in public repos | 2 min |
Origin story: An AI agent ran
git checkout --on files containing hours of uncommitted work from a parallel coding session. The files were recovered viagit fsck --lost-found, but this prompted creating a mechanical enforcement system.
A Python hook for Claude Code that intercepts Bash commands and blocks destructive operations before they execute.
Blocked commands:
| Command | Why it's dangerous |
|---|---|
git checkout -- <files> |
Discards uncommitted changes permanently |
git reset --hard |
Destroys all uncommitted work |
git clean -f |
Deletes untracked files |
git push --force |
Overwrites remote history |
rm -rf (non-temp paths) |
Recursive deletion |
How it works: Claude Code's PreToolUse hook system runs the guard script before each Bash command. The script pattern-matches against known destructive commands and returns a deny decision with an explanation. Safe variants (like git checkout -b, git clean -n, rm -rf /tmp/...) are allowlisted.
Quick install
See the full guide for the automated installer. After running it:
# Restart Claude Code for hooks to take effectTest that it works:
echo '{"tool_name": "Bash", "tool_input": {"command": "git checkout -- file.txt"}}' | \
python3 .claude/hooks/git_safety_guard.py
# Should output: permissionDecision: denyProblem: During long coding sessions, Claude Code compacts the conversation to stay within context limits. After compaction, Claude "forgets" project-specific conventions documented in AGENTS.md.
A bash hook that detects context compaction and injects a reminder for Claude to re-read AGENTS.md.
How it works: Claude Code's SessionStart hook fires with source: "compact" after compaction. The hook checks this field and outputs a reminder message that Claude sees immediately.
Quick install
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/misc_coding_agent_tips_and_scripts/main/install-post-compact-reminder.sh | bash
# Restart Claude Code after installationOr manually create ~/.local/bin/claude-post-compact-reminder:
#!/usr/bin/env bash
set -e
INPUT=$(cat)
SOURCE=$(echo "$INPUT" | jq -r '.source // empty')
if [[ "$SOURCE" == "compact" ]]; then
cat <<'EOF'
<post-compact-reminder>
Context was just compacted. Please reread AGENTS.md to refresh your understanding of project conventions and agent coordination patterns.
</post-compact-reminder>
EOF
fi
exit 0Then add to ~/.claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{ "type": "command", "command": "$HOME/.local/bin/claude-post-compact-reminder" }
]
}
]
}
}When using Ghostty to SSH into remote servers, you might see garbage like [57414u when pressing numpad Enter. This happens because the remote system doesn't understand the Kitty keyboard protocol that Ghostty uses.
One-liner fix:
infocmp -x xterm-ghostty | ssh user@your-server 'mkdir -p ~/.terminfo && tic -x -o ~/.terminfo -'Helper function for multiple servers
Add to ~/.zshrc:
ghostty_push_terminfo() {
local host="$1"
[[ -z "$host" ]] && { echo "Usage: ghostty_push_terminfo <host>" >&2; return 1; }
infocmp -x xterm-ghostty | ssh "$host" 'mkdir -p ~/.terminfo && tic -x -o ~/.terminfo -'
}Then: ghostty_push_terminfo ubuntu@dev-server
When you have terminals open to multiple servers, color-coding each connection prevents accidentally running commands on the wrong machine.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ PURPLE │ │ AMBER │ │ CRIMSON │
│ dev-server │ │ staging │ │ production │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Two approaches:
| Ghostty/Shell | WezTerm/Lua | |
|---|---|---|
| Setup time | ~5 min | ~15 min |
| Works in | Any terminal with OSC support | WezTerm only |
| Tab bar theming | No | Yes |
| Gradient backgrounds | No | Yes |
| Session persistence | No | Yes (survives disconnects) |
Ghostty quick setup
Add to ~/.zshrc:
my-server() {
printf '\e]11;#1a0d1a\a\e]10;#e8d4f8\a\e]12;#bb9af7\a' # purple theme
ssh ubuntu@my-server.example.com "$@"
printf '\e]111\a\e]110\a\e]112\a\e]104\a' # reset on exit
}The printf commands send OSC escape sequences that change terminal colors. \e]11;#1a0d1a\a sets background; \e]111\a resets it.
Remote terminal sessions that survive Mac sleep, reboot, or power loss. Uses WezTerm's native multiplexing with wezterm-mux-server running on remote servers via systemd.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Local │ │ Dev │ │ Staging │ │ Workstation│
│ [3 tabs] │ │ [3 tabs] │ │ [3 tabs] │ │ [3 tabs] │
│ (fresh) │ │ (persistent) │ │ (persistent) │ │ (persistent) │
└──────────────┘ └──────────────┘ └──────────────┘ └───────────────┘
Key features:
| Feature | How It Works |
|---|---|
| Persistent sessions | wezterm-mux-server on remote holds state; Mac just reconnects |
| Smart startup | Doesn't accumulate tabs on restart (checks if remote has existing tabs) |
| Domain-specific colors | Each server gets distinct gradient + tab bar theme |
| Better than tmux | Native scrollback, single keybinding namespace, GPU rendering |
Remote setup (per server)
# Create systemd user service
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/wezterm-mux-server.service << 'EOF'
[Unit]
Description=WezTerm Mux Server
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/wezterm-mux-server --daemonize=false
Restart=on-failure
[Install]
WantedBy=default.target
EOF
# Enable and start
systemctl --user daemon-reload
systemctl --user enable --now wezterm-mux-server
sudo loginctl enable-linger $USERWhen running 20+ AI coding agents (Claude, Codex) simultaneously, the default wezterm-mux-server configuration can't keep up. Output buffers overflow, caches thrash, and connections time out, killing all your agent sessions.
This guide provides RAM-tiered performance profiles that trade memory for throughput:
| Setting | Default | 512GB Profile | Why It Helps |
|---|---|---|---|
scrollback_lines |
3,500 | 10,000,000 | Agents produce massive output; don't truncate |
mux_output_parser_buffer_size |
128KB | 16MB | Batch-process output bursts instead of choking |
mux_output_parser_coalesce_delay_ms |
3ms | 1ms | Reduce accumulated lag on high-throughput output |
shape_cache_size |
1,024 | 65,536 | Cache font shaping to avoid CPU spikes |
Smart sizing: Uses linear interpolation based on actual RAM, not fixed tiers. A 200GB system gets proportionally scaled settings.
Bonus: Emergency rescue procedure to migrate agent sessions to tmux using reptyr when the mux server becomes unresponsive (saves ~50-70% of sessions).
Quick install
./wezterm-mux-tune.sh # Auto-detect RAM, interpolate
./wezterm-mux-tune.sh --dry-run # Preview settings
./wezterm-mux-tune.sh --ram 200 # Specific RAM amount
./wezterm-mux-tune.sh --profile 256 # Fixed profile
./wezterm-mux-tune.sh --restore # Restore backupThen restart the mux server:
pkill -9 -f wezterm-mux
wezterm-mux-server --daemonizeFull guide → | Install script →
The horizontal thumbwheel on Logitech MX Master mice is designed for horizontal scrolling, which most developers rarely use. This guide repurposes it for tab switching across terminals, editors, and browsers.
Setup:
- Install BetterMouse ($10 one-time)
- Map thumbwheel to
Ctrl+Shift+Arrow:Thumbwheel << → Ctrl+Shift+Left Thumbwheel >> → Ctrl+Shift+Right - Add keybindings to each app (WezTerm, Ghostty, VS Code, etc.)
Companion script: bettermouse_config.py exports and imports BetterMouse settings as JSON for backup or sharing. Run with uv run bettermouse_config.py show to view your current config.
Why Ctrl+Shift+Arrow?
| Shortcut | Problem |
|---|---|
Ctrl+Tab |
Can't rebind in Chrome |
Cmd+[ / Cmd+] |
Used for navigation history |
Cmd+Shift+[ / Cmd+Shift+] |
Used for tab switching in some apps, but not universal |
Ctrl+Shift+Arrow |
Rarely used as a default; easy to rebind everywhere |
A vibrant cyberpunk-inspired color scheme for Ghostty featuring deep space black backgrounds with electric neon accents.
┌─────────────────────────────────────────────────────┐
│ Background: #0a0e14 (deep space black) │
│ Foreground: #b3f4ff (electric cyan) │
│ Cursor: #ff00ff (hot magenta) │
│ │
│ Palette highlights: │
│ Red: #ff3366 → #ff6b9d (electric pink) │
│ Green: #39ffb4 → #6bffcd (neon teal) │
│ Blue: #00aaff → #66ccff (cyber blue) │
│ Magenta:#ff00ff → #ff66ff (hot purple) │
└─────────────────────────────────────────────────────┘
Quick install:
# Copy theme to Ghostty themes directory
mkdir -p ~/.config/ghostty/themes
cp doodlestein-punk-theme-for-ghostty ~/.config/ghostty/themes/Usage: Add to your Ghostty config (~/.config/ghostty/config):
theme = doodlestein-punk-theme-for-ghostty
Full palette
| Index | Normal | Bright | Color Name |
|---|---|---|---|
| 0/8 | #1a1f29 |
#3d4f5f |
Black/Gray |
| 1/9 | #ff3366 |
#ff6b9d |
Red/Pink |
| 2/10 | #39ffb4 |
#6bffcd |
Green/Teal |
| 3/11 | #ffe566 |
#ffef99 |
Yellow |
| 4/12 | #00aaff |
#66ccff |
Blue/Cyan |
| 5/13 | #ff00ff |
#ff66ff |
Magenta |
| 6/14 | #00ffff |
#66ffff |
Cyan |
| 7/15 | #c7d5e0 |
#ffffff |
White |
Problem: When SSH'd into a remote Linux machine running Zellij as the terminal multiplexer, the mouse scroll wheel sends arrow keys instead of scrolling the terminal buffer. If you use atuin for shell history, the up arrow opens a full-screen history TUI on every scroll.
This is a known Zellij limitation — Zellij receives proper mouse scroll events but converts them to arrow key presses. The workaround uses Hammerspoon on macOS to intercept scroll wheel events and convert them to Alt+Up/Alt+Down keystrokes, which Zellij handles as scroll commands.
Scroll wheel → Hammerspoon (macOS) → Alt+Up/Down → SSH → Zellij → ScrollUp/ScrollDown
Three components:
| Component | Where | What It Does |
|---|---|---|
| Zellij keybinds | Remote server | Alt+Up/Alt+Down → ScrollUp/ScrollDown in locked mode |
| Hammerspoon | Mac | Scroll wheel → Alt+Up/Alt+Down when terminal is focused |
| Atuin flag | Remote server | --disable-up-arrow prevents stray arrows from triggering history |
Quick Zellij config
keybinds clear-defaults=true {
locked {
bind "Ctrl g" { SwitchToMode "Normal"; }
bind "Alt Up" { ScrollUp; }
bind "Alt Down" { ScrollDown; }
}
// ...
}Hammerspoon gotchas
Three things that will silently break your event tap:
localvariables get garbage collected by Lua, killing the tap after 30-90 min. Use global variables.- Calling
:post()orkeyStroke()inside the callback makes it too slow. Return events as a table (second return value) instead. - macOS
tapDisabledByTimeoutkills slow taps. Add a watchdog timer that re-enables it every 5 seconds.
The guide also covers why WezTerm's native mouse_bindings don't reliably work for this, and how to tune scroll sensitivity with smooth scrolling mice.
If you have a remote Linux workstation with projects at /data/projects, you can make it automatically mount on your Mac at boot with graceful retry logic.
What you get:
~/dev-projects → /Volumes/dev-server/projects → 10.0.0.50:/data/projects
Components:
| Component | Purpose |
|---|---|
| Mount script | Retries with exponential backoff when server is offline |
| LaunchDaemon | Runs at boot, re-mounts on network changes |
| Symlink | Convenient ~/dev-projects path |
| Synthetic firmlink | Optional /dev/projects root-level path |
Quick setup
# Create mount script
sudo tee /usr/local/bin/mount-dev-nfs << 'EOF'
#!/bin/bash
REMOTE_HOST="10.0.0.50"
MOUNT_POINT="/Volumes/dev-server"
[ -d "$MOUNT_POINT" ] || mkdir -p "$MOUNT_POINT"
mount | grep -q "$MOUNT_POINT" && exit 0
ping -c 1 -W 1 "$REMOTE_HOST" &>/dev/null || exit 1
/sbin/mount_nfs -o resvport,rw,soft,intr,bg "$REMOTE_HOST:/data" "$MOUNT_POINT"
EOF
sudo chmod +x /usr/local/bin/mount-dev-nfs
# Create LaunchDaemon (see full guide for complete plist)
# Then: sudo launchctl load /Library/LaunchDaemons/com.local.mount-dev-nfs.plist
# Create symlink
ln -sf /Volumes/dev-server/projects ~/dev-projectsConnect your Mac directly to a Linux workstation with 10GbE for ~$90 total, achieving 800+ MB/s transfers.
Mac Mini M4 Linux Workstation
┌─────────────┐ ┌─────────────────┐
│ Thunderbolt │◄──Cat6 $5────►│ Built-in 10GbE │
│ 10GbE ~$85 │ │ (Aquantia) │
└─────────────┘ └─────────────────┘
Static IPs: 10.10.10.x | Speed: ~850 MB/s
What you need:
| Component | Cost |
|---|---|
| IOCREST Thunderbolt 10GbE adapter | ~$85 (AliExpress) |
| Cat6 cable | ~$5 |
Many high-end workstations (Threadripper PRO, EPYC) have unused 10GbE ports. The IOCREST adapter uses the same Aquantia chip as Mac Studio's built-in 10GbE.
Also includes:
- SHA-256 verified file transfers with speed reporting
- Clipboard sync between Mac and Linux (Wayland/X11)
- Remote display wake-up commands
- AI coding agent aliases (Claude, Gemini, Codex)
After installing Claude Code via curl -fsSL https://claude.ai/install.sh | bash, you might still see the old version if a previous bun/npm installation is earlier in your PATH.
Symptoms:
claude --versionshows old version- "Auto-update failed" errors
claude doctorshows "Currently running: unknown"
Fix:
# Use explicit path in aliases
alias cc='~/.local/bin/claude --dangerously-skip-permissions'
# Update alias uses native updater
alias uca='~/.local/bin/claude update'
# Remove stale symlinks
rm ~/.bun/bin/claude 2>/dev/nullClaude Code stores MCP server configurations in ~/.claude/settings.json on current installs, though older installs may still use ~/.claude.json. These can get wiped out by fresh installs, updates, or config corruption. Instead of running the full MCP Agent Mail installer, use this lightweight script that only restores the MCP config.
One command fix:
fix_cc_mcpWhat it restores:
| Server | Type | Purpose |
|---|---|---|
mcp-agent-mail |
HTTP | Multi-agent coordination, messaging, file reservations |
morph-mcp |
stdio | AI-powered grep-only Morph search via warpgrep_codebase_search,warpgrep_github_search |
Quick install
# Download and install the script
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/misc_coding_agent_tips_and_scripts/main/FIX_CLAUDE_CODE_MCP_CONFIG.md | \
sed -n '/^SCRIPT$/,/^SCRIPT$/p' | sed '1d;$d' > ~/.local/bin/fix_cc_mcp
chmod +x ~/.local/bin/fix_cc_mcp
# Edit to add your Morph API key
nano ~/.local/bin/fix_cc_mcpOr copy the script from the full guide.
Token discovery: The script automatically finds your bearer token from MCP_AGENT_MAIL_TOKEN, ~/mcp_agent_mail/.env, current ~/.claude/settings.json, or legacy ~/.claude.json.
Claude Code skills are directories containing a SKILL.md file, stored in .claude/skills/. Project-local skills only work in that project, but global skills in ~/.claude/skills/ are available everywhere. This script mirrors skills from a project to the global directory using rsync.
Usage:
mirror_cc_skills # Mirror from current project
mirror_cc_skills /path/to/project # Mirror from specific project
mirror_cc_skills --dry-run # Preview changes
mirror_cc_skills --sync # Delete skills not in source (backs up first)Behavior:
| Mode | What it does |
|---|---|
| Default | Only adds/updates skills, never deletes from destination |
--sync |
Full sync: deletes skills not in source (creates timestamped backup first) |
--dry-run |
Shows what would be copied without making changes |
Quick install
# Download and install
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/misc_coding_agent_tips_and_scripts/main/mirror_cc_skills \
-o ~/.local/bin/mirror_cc_skills
chmod +x ~/.local/bin/mirror_cc_skillsOr copy from this repo:
cp mirror_cc_skills ~/.local/bin/
chmod +x ~/.local/bin/mirror_cc_skillsNote: The script automatically installs gum on first run for prettier output (styled boxes and spinners). Works fine without it if installation fails.
Beads uses git worktrees for sync operations. If your sync.branch is set to your current branch, you'll get:
fatal: 'main' is already checked out at '/path/to/repo'
Fix: Create a dedicated sync branch:
git branch beads-sync main
git push -u origin beads-sync
bd config set sync.branch beads-syncGoogle's Gemini CLI (@google/gemini-cli) has two bugs that make it nearly unusable in practice:
-
EBADF crash on every launch (wide terminals): The CLI uses
node-ptyfor shell execution. AuseEffectin the shell tool component firesresizePty()after the PTY's file descriptor is already closed. The native C++ addon throwsError("ioctl(2) failed, EBADF"), but the catch blocks only checkerr.code === 'ESRCH'— the native addon sets no.codeproperty (only.message), so the error falls through and crashes the entire CLI. -
"Sorry there's high demand" gives up after 10 tries: The default retry config (
DEFAULT_MAX_ATTEMPTS = 10,maxDelayMs = 30000) means Gemini gives up quickly during high-demand periods. Worse,TerminalQuotaError(which fires for daily limits and temporary overload) bypasses retry entirely and immediately surrenders.
One-liner fix:
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/misc_coding_agent_tips_and_scripts/main/fix-gemini-cli-ebadf-crash.sh | bashWhat it patches (7 patches total — 5 node_modules + 2 environment, all idempotent):
| Patch | File | Change |
|---|---|---|
| EBADF catch #1 | shellExecutionService.js |
Add err.message?.includes('EBADF') to resizePty() catch |
| EBADF catch #2 | ShellToolMessage.js |
Add EBADF + ESRCH checks to shell tool resize useEffect |
| Aggressive retry | retry.js |
maxAttempts 10 → 1000, initialDelay 5s → 1s, maxDelay 30s → 5s |
| Never bail on quota | retry.js |
TerminalQuotaError retries with backoff instead of immediately giving up |
| EBADF at source | unixTerminal.js |
Wrap native pty.resize() in try/catch for EBADF/ESRCH |
| Dead hooks | ~/.gemini/settings.json |
Remove hooks pointing to nonexistent binaries |
| PATH order | ~/.local/bin/gmi |
Ensure /usr/bin before .bun/bin so real node is used |
⚠️ v0.36.0+ bundle architecture
Starting with v0.36.0, gemini-cli ships as a self-contained bundle — all core logic (retry, shell execution) is compiled into chunk files with mangled identifiers. Patches 1–4 target @google/gemini-cli-core source files which are no longer used at runtime by the bundle build.
What still works in v0.36.0+:
- Patch 5 (unixTerminal.js) —
@lydell/node-ptyis a native addon loaded at runtime, not bundled. This catches EBADF at the source before any higher-level code sees it. This is the primary EBADF fix for bundle builds. - Patches 6–7 — Environment fixes (settings.json, gmi wrapper) are independent of the package architecture.
What doesn't apply: Patches 1–4 cannot modify the compiled bundle. If gemini-cli-core is still installed (leftover from a prior version), the script will patch it but notes it's not used at runtime.
Other modes
./fix-gemini-cli-ebadf-crash.sh --check # check if patches are needed (no changes)
./fix-gemini-cli-ebadf-crash.sh --verify # reproduce the EBADF bug + show retry config
./fix-gemini-cli-ebadf-crash.sh --revert # undo all patches
./fix-gemini-cli-ebadf-crash.sh --uninstall # same as --revertHow the EBADF bug was diagnosed
$ node -e "const pty = require('@lydell/node-pty-linux-x64/pty.node'); \
try { pty.resize(-1, 80, 24); } catch(e) { \
console.log('message:', e.message); \
console.log('code:', e.code); \
console.log('has code:', 'code' in e); }"
message: ioctl(2) failed, EBADF
code: undefined
has code: falseThe native addon throws a plain Error with "EBADF" only in the message string. The existing catch checks err.code === 'ESRCH' which is undefined === 'ESRCH' → false. The error falls through and crashes React's commit phase.
Note: Patches live in node_modules and will be overwritten by package updates. Re-run the script after bun update -g @google/gemini-cli.
Vercel's automatic deployments on every push can burn through Pro plan credits quickly. Use the REST API to disable auto-deployments and take control of when builds happen.
Before:
git push → Vercel webhook → Build → Deploy → 💸 (every single push)
After:
git push → (nothing)
vercel --prod → Build → Deploy → 💸 (only when you're ready)
API command to disable auto-deploys:
curl -X PATCH "https://api.vercel.com/v9/projects/${PROJECT_ID}?teamId=${TEAM_ID}" \
-H "Authorization: Bearer ${VERCEL_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"gitProviderOptions": {"createDeployments": "disabled"}}'Additional optimizations
| Setting | API Field | Effect |
|---|---|---|
| Disable auto-deploy | gitProviderOptions.createDeployments |
No deploys on push/PR |
| Smart skip | enableAffectedProjectsDeployments |
Skip unchanged monorepo projects |
| Custom check | commandForIgnoringBuildStep |
Run script to decide |
Master the CLI for each cloud platform instead of clicking through web dashboards. This guide covers installation, authentication, and common commands for the tools you use daily.
| Tool | Purpose |
|---|---|
gh |
GitHub PRs, issues, releases |
vercel |
Deployments, logs, env vars |
wrangler |
Cloudflare Workers, R2 storage |
gcloud |
Google Cloud APIs, billing |
supabase |
Database migrations, types |
Quick install (all tools)
# GitHub CLI
brew install gh
gh auth login
# Vercel
bun add -g vercel
vercel login
# Cloudflare Wrangler
bun add -g wrangler
wrangler login
# Supabase
bun add -g supabase
supabase loginThe guide also includes AGENTS.md blurbs for each tool with placeholders for your project-specific values.
Setup for streaming from a Linux workstation (Hyprland/Wayland, dual RTX 4090) to a Mac client using Moonlight with AV1 encoding.
| Component | Configuration |
|---|---|
| Server | Sunshine on Hyprland with NVENC |
| Client | Custom Moonlight build with AV1 |
| Resolution | 3072x1728 @ 30fps |
| Codec | AV1 (requires RTX 40-series) |
Shell aliases
ml # Start Moonlight streaming
trj # SSH into remote server
wu # Wake up remote display
cptl # Copy clipboard to Linux
cpfm # Copy clipboard from MacCommon issues: Display sleep disconnects GPU from DRM, causing "GPU doesn't support AV1" errors. Fix: enable NVIDIA persistence mode (nvidia-smi -pm 1) and disable hypridle.
The problem. Run a few Claude or Codex agents on a fat workstation, give them an hour, then check free -h:
total used free shared buff/cache available
Mem: 215Gi 66Gi 39Gi 26Gi 150Gi 149Gi
Swap: 71Gi 71Gi 0B
149 GB of RAM available, 71 GB of swap full. The system isn't currently under memory pressure — but an earlier spike (a build, a batch of tests, a model briefly spinning up) evicted ~71 GB of pages to disk. The Linux kernel doesn't proactively migrate those pages back. They sit on disk until something touches them, at which point that read pays a slow random-disk seek. The machine feels sluggish for hours after the actual spike is gone, even though top shows nothing wrong. This is the swap paradox.
The fix. sudo swapoff -a && sudo swapon -a walks every swapped-out page back into RAM in one bulk operation and re-enables swap fresh. Costs a few minutes of mild I/O; restores responsiveness. But it has a sharp edge: the flush itself can OOM the box if you don't have enough free RAM to absorb the swapped data. So you can't just slap it in cron.
This tool is the safe automatic version. A systemd timer fires every 30 minutes and runs a small bash script that decides whether to flush based on four predicates:
| Predicate | Default | Protects against |
|---|---|---|
MIN_DISK_SWAP_GB |
4 GB | Doing the work for a trivial reclaim |
MEM_SAFETY_FACTOR |
1.5× | OOM during the migrate-pages-back operation |
MAX_MEM_PRESSURE |
5.0 % PSI avg10 | Piling work onto a memory-stressed host |
MAX_LOAD_RATIO |
1.0× nproc | Piling work onto a CPU-stressed host |
If all four hold, the script runs swapoff -av && swapon -av and logs the before/after numbers. Otherwise it logs a skip reason and exits cleanly. Either way, exit code 0 — so systemctl status swap-flush.service only goes red on real malfunctions, never on a deliberate skip.
A few smaller design decisions worth highlighting:
- zram swap doesn't count. zram is RAM-backed compressed pages, not on disk. There's no random-read latency to restore by flushing it. If your host uses both zram + disk swap, only the disk portion is what we want to drain.
MEM_SAFETY_FACTORis the OOM gate. With the default 1.5×, on a system with 30 GB of disk swap used, the script requires 45 GB of available RAM before it triesswapoff -a. That's the headroom needed for the kernel to migrate pages back without crashing into normal allocations.- Numeric env vars are validated at startup. A typo like
MEM_SAFETY_FACTOR="1,5"(comma instead of period) would otherwise make awk silently parse it as 0, defeating the safety check and proceeding with zero headroom. A regex catches this and aborts before any swap manipulation. - 5-minute random jitter prevents fleet-wide hosts from all flushing at exactly
:00and:30. A 13-machine swarm doesn't need 13 simultaneous swapoff storms. Type=oneshotensures only one instance runs at a time; the next timer fire waits if the current run is still draining.
Tunable per-host via /etc/systemd/system/swap-flush.service.d/override.conf (or systemctl edit swap-flush.service). All decisions go to journald under tag swap-flush:
journalctl -t swap-flush --since todayQuick install
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/misc_coding_agent_tips_and_scripts/main/install-swap-flush.sh | sudo bashDrops /usr/local/bin/swap-flush, the .service, and the .timer into place. Enables the timer. First fire lands within 5 minutes; then every 30 min thereafter (with up to 5 min of random jitter per fire).
Preview without making changes:
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/misc_coding_agent_tips_and_scripts/main/install-swap-flush.sh | sudo bash -s -- --dry-runRun it once right now (manually; still respects all four safety predicates):
sudo /usr/local/bin/swap-flushOverride defaults per-host:
sudo systemctl edit swap-flush.service
# [Service]
# Environment=MEM_SAFETY_FACTOR=2.0Uninstall:
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/misc_coding_agent_tips_and_scripts/main/install-swap-flush.sh | sudo bash -s -- --uninstallFull guide → | Script → | Installer →
A highly available secrets manager using 3-node Raft consensus. If the leader fails, the cluster elects a new leader and keeps running.
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ NODE 1 │ │ NODE 2 │ │ NODE 3 │
│ (Leader) │ │ (Follower) │ │ (Follower) │
│ 10.0.1.10 │ │ 10.0.1.11 │ │ 10.0.1.12 │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
└──────────────────────┼──────────────────────┘
Raft Consensus
Why Integrated Raft:
| Benefit | Description |
|---|---|
| No external dependencies | Storage built into Vault |
| Automatic failover | Leader election in seconds |
| Consistent replication | All nodes have the same data |
Initialize cluster (first node only)
export VAULT_ADDR='http://127.0.0.1:8200'
# Initialize with Shamir's Secret Sharing
vault operator init -key-shares=5 -key-threshold=3
# Save the unseal keys and root token securely!
# You need 3 of 5 keys to unseal after restart.
# Unseal
vault operator unseal # x3 with different keysCheck cluster health
vault operator raft list-peers
# Node Address State Voter
# node1 10.0.1.10:8201 leader true
# node2 10.0.1.11:8201 follower true
# node3 10.0.1.12:8201 follower trueProblem: You want to accept sensitive security reports (vulnerabilities, credentials, PII) through public GitHub issues without exposing the content to anyone except you and your coding agents.
The solution uses age, a modern X25519 elliptic-curve encryption tool. Reporters encrypt to your public key; only your private key can decrypt. No shared secrets, no out-of-band key exchange, no GPG complexity.
Coding agents: Run gh-issue-decrypt with no arguments for an interactive guide that walks through the encrypt/submit/scan/decrypt workflow and detects your local keys automatically.
Reporter Your Coding Agent
──────── ─────────────────
1. Gets your public key from README 4. Scans issues (gh api)
2. Encrypts with age -a -r PUBKEY 5. Detects armored age blocks
3. Pastes ciphertext in GitHub issue 6. Decrypts with age -d -i key
One-liner install (sets up age, gh CLI, and generates your keypair):
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/misc_coding_agent_tips_and_scripts/main/gh-issue-decrypt?$(date +%s)" | bash -s -- --installFor reporters submitting an encrypted issue:
# Install age if needed, then encrypt and submit in one command:
echo "SSRF vulnerability in /api/proxy — allows internal network scanning via user-controlled URL parameter" \
| gh-issue-decrypt --encrypt age1YOUR_PUBKEY_HERE --submit OWNER/REPO --title "Security: SSRF in proxy endpoint"Or manually, encrypt and paste:
echo "your secret report" | age -a -r age1PUBKEY_HERE
# Paste the output into a GitHub issue wrapped in [enc:age]...[/enc:age] markers| Feature | Details |
|---|---|
| Encryption | X25519 (Curve25519 ECDH), 128-bit security level |
| Key format | age1... public key (62 chars, safe to publish) |
| Ciphertext | ASCII-armored, Markdown-safe, paste-friendly |
| Dependencies | age + gh (auto-installed on Linux/macOS) |
| Package managers | apt, dnf, pacman, apk, zypper, nix, Homebrew, MacPorts, GitHub binary fallback |
| Agent integration | --json mode for machine-readable output |
| Sender mode | --encrypt encrypts stdin; --submit creates the issue directly |
How reporters encrypt (step by step)
# 1. Install age
brew install age # macOS
sudo apt install age # Ubuntu/Debian
# Or let the script handle it:
gh-issue-decrypt --install
# 2. Get the project's public key from their README
# It looks like: age1k5fz7d7zqsd4k60rn024a3gsd4tumjrkaqna0r20jz4gsu7mmvvql2k3jr
# 3. Encrypt your message (armored for pasting)
echo "The auth middleware at line 42 skips token expiry validation
when the iss claim is missing. An attacker can forge expired JWTs
by omitting the issuer field." \
| age -a -r age1k5fz7d7zqsd4k60rn024a3gsd4tumjrkaqna0r20jz4gsu7mmvvql2k3jr
# 4. Paste the output into a GitHub issue:
#
# [enc:age]
# -----BEGIN AGE ENCRYPTED FILE-----
# YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA...
# -----END AGE ENCRYPTED FILE-----
# [/enc:age]How your agents decrypt
# Scan all open issues in a repo
gh-issue-decrypt Dicklesworthstone/some_project
# Decrypt a specific issue
gh-issue-decrypt Dicklesworthstone/some_project 42
# Machine-readable output for automation
gh-issue-decrypt --json Dicklesworthstone/some_project
# Manual decryption of a single block
age -d -i ~/.config/age/issuebot.key <<'EOF'
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA...
-----END AGE ENCRYPTED FILE-----
EOFAgent quickstart (run with no args)
Running gh-issue-decrypt with no arguments prints a guide covering the full workflow (encrypt, submit, scan, decrypt). Designed for agents like Claude Code, Codex, or Gemini CLI that need to understand the system from scratch:
gh-issue-decrypt
# Prints:
# - How public-key encryption works in this context
# - Step-by-step sender instructions
# - Three submission methods (--submit, gh issue create, manual paste)
# - Receiver scanning commands
# - All available flagsAuthentication caveat
age encrypts but does not authenticate the sender. Anyone with your public key can encrypt to you. If you need to verify who sent a message, have senders also sign their plaintext with:
- SSH signatures (
ssh-keygen -Y sign): easiest if they already have SSH keys - minisign (
minisign -S): lightweight Ed25519 signing tool
Why age over GPG/minisign/libsodium
- GPG/OpenPGP: Works but operationally fragile. Key management is a UX disaster. Even SOPS recommends age over PGP where possible.
- minisign: Signing only, not encryption. Good complement to age for sender authentication.
- libsodium/NaCl box: Good primitives but you end up designing a custom message format, detection convention, and CLI tooling yourself.
- age: Simple CLI, tiny keys, Unix-composable, ASCII-armored output safe for Markdown, and the recipient-key model means no shared secrets.
Script source → | Full Claude Code session transcript → (the full Claude Code session that built this system: 19 human prompts, ~400 tool calls, from initial concept through fleet testing)
| Category | Tools |
|---|---|
| AI Agents | Claude Code, Codex, Gemini CLI |
| Terminals | WezTerm, Ghostty, iTerm2 |
| Editors | VS Code, Zed |
| Version Control | Git (with safety hooks) |
| Remote Access | NFS, SSH, Moonlight, Sunshine |
| Infrastructure | HashiCorp Vault, systemd |
| Platforms | Vercel |
| Package Managers | bun, npm, native installers |
| Hardware | NVIDIA GPUs, Logitech MX Master |
| Security | age (X25519 encryption) |
| OS | macOS, Linux (Ubuntu, Arch) |
- Claude Code Documentation
- MCP Agent Mail - Multi-agent coordination server
- Morph MCP - AI-powered code search
- Beads Project
- Moonlight / Sunshine
- BetterMouse
- WezTerm / Ghostty
- HashiCorp Vault / Vault Tutorials
- Vercel REST API
- age encryption — Simple, modern X25519 encryption
Last updated: March 2026