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 |
| 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 3 retries | 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 [email protected] "$@"
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.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 code search via warp_grep |
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 env var, ~/mcp_agent_mail/.env, or existing ~/.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 (4 patches across 3 files, 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 |
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.
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