Posts in category “Tips”

tmux send-keys silently drops the final Enter

When dispatching commands between tmux sessions, tmux send-keys -t <target> '<command>' Enter exits 0 and the command text appears in the target's input buffer — but the Enter never commits. The agent sits idle. The dispatcher sees success. Nothing happened.

This hits hardest with long commands that trigger tmux's bracketed paste mode: the trailing Enter gets absorbed into the paste block instead of executing it. Two other failure modes: the target app is in a modal state (dialog, secondary prompt) that swallows Enter, or the input handler is loaded and drops the keystroke.

The fix is a verify-and-retry loop after every dispatch:

dispatch() {
  local target="$1" cmd="$2"

  # 1. Send
  tmux send-keys -t "$target" "$cmd" Enter

  # 2. Wait for the target to start processing
  sleep 3

  # 3. Verify it's actually working, not just staged
  if tmux capture-pane -t "$target" -p -S -10 | tail -10 \
      | grep -qE "Working|Exploring|Reading|Analyzing"; then
    return 0
  fi

  # 4. Retry — empty Enter kicks the paste buffer
  tmux send-keys -t "$target" "" Enter
  sleep 2

  # One more check, then give up
  if tmux capture-pane -t "$target" -p -S -10 | tail -10 \
      | grep -qE "Working|Exploring|Reading|Analyzing"; then
    return 0
  fi

  # 5. Fallback: write to file, let target poll for it
  echo "$cmd" > "/tmp/dispatch-${target##*.}.cmd"
  return 1
}

Cap retries at 2. If both fail, fall back to writing the command to a file the target reads on its own polling cycle — this sidesteps the pty entirely.

The broader lesson: any async channel where the sender gets a local exit code instead of a structured delivery ACK has this failure class. Pty pipes, shell sessions, anything interactive. If you're building multi-agent coordination over interactive channels, bake verification into the dispatch protocol and don't trust exit code 0.

Apply the same pattern in reverse for callbacks: workers should verify their "done" message was received by the dispatcher, not just fire-and-forget.

AI coding CLIs all support a custom config directory

When you need a clean slate — testing, debugging, or isolating a project — each major AI coding CLI lets you override its config directory via an environment variable:

# Claude Code
CLAUDE_CONFIG_DIR=/tmp/claude-clean claude

# GitHub Copilot CLI
COPILOT_HOME=/tmp/copilot-clean copilot

# OpenAI Codex CLI
CODEX_HOME=/tmp/codex-clean codex

These replace the entire config directory (~/.claude/, ~/.copilot/, ~/.codex/), so existing sessions, permissions, and plugins won't carry over. Copy the default directory contents if you need to preserve anything.

Copilot also has a separate COPILOT_CACHE_HOME for marketplace cache and update packages — setting COPILOT_HOME alone won't relocate those.

One IP, Multiple HTTPS Domains on Nginx — SNI Solves It

In the HTTP era, virtual hosting was trivial: one IP serves many domains, the Host header tells them apart. HTTPS breaks this because TLS handshake happens before any HTTP header — the server must pick a certificate before knowing which domain the client wants.

SNI — the standard fix

Server Name Indication is a TLS extension where the client sends the target hostname during handshake. All modern clients support it. Nginx uses it automatically — just define separate server blocks:

server {
    listen 443 ssl;
    server_name a.com;
    ssl_certificate /etc/ssl/a.com.crt;
    ssl_certificate_key /etc/ssl/a.com.key;
}

server {
    listen 443 ssl;
    server_name b.com;
    ssl_certificate /etc/ssl/b.com.crt;
    ssl_certificate_key /etc/ssl/b.com.key;
}

Each domain gets its own certificate. Nginx routes based on SNI. Zero extra config needed.

When you'd rather use one certificate

For a handful of related domains, a SAN certificate (Subject Alternative Name) covers multiple names in one cert:

server {
    listen 443 ssl;
    server_name a.com b.com c.com;
    ssl_certificate /etc/ssl/multi.crt;
    ssl_certificate_key /etc/ssl/multi.key;
}

Let's Encrypt makes this painless:

certbot --nginx -d a.com -d b.com -d c.com

For lots of subdomains (*.example.com), a wildcard certificate is the way to go.

SNI + Let's Encrypt covers 99% of real-world setups — free and automatic.

Run Multiple Claude Code Instances with Separate Configs

Use CLAUDE_CONFIG_DIR to point Claude Code at a different config directory — credentials, settings, sessions all go there instead of ~/.claude.

mkdir -p ~/.claude-work
CLAUDE_CONFIG_DIR=$HOME/.claude-work claude

Set up aliases in your shell config for quick switching:

alias claude-work='CLAUDE_CONFIG_DIR=$HOME/.claude-work claude'

First launch in the new directory triggers the login flow — that's how you get account isolation. Both instances can run simultaneously without interference.

Share session history between instances

CLAUDE_CONFIG_DIR is all-or-nothing — no built-in way to share just sessions. Symlink the projects/ directory back to the original to get shared session history with separate settings:

mkdir -p ~/.claude-work
cp ~/.claude/settings.json ~/.claude-work/settings.json
ln -s ~/.claude/projects ~/.claude-work/projects

This gives you independent config/credentials but a unified session list. New sessions written by either instance appear in both (bidirectional). No read-only option exists — if you need isolation, don't symlink.

WSL2 Won't Run on EC2 Windows Server? Use WSL1

Most EC2 Windows Server instances don't expose nested virtualization to the guest OS. WSL2 requires Hyper-V extensions, so it simply won't start. WSL1 works fine — it's a translation layer, not a VM.

Enable the WSL feature and reboot in an Admin powershell window:

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux
Restart-Computer

After reboot, pin the default version to 1:

wsl --set-default-version 1

On Windows Server, Add-AppxPackage is often broken or unavailable, so the standard wsl --install route fails. Use wsl --import with a rootfs tarball instead:

Now use a non-admin powershell:

mkdir $HOME\WSL
mkdir $HOME\WSL\Ubuntu

Then:

wsl --import Ubuntu $HOME\WSL\Ubuntu .\ubuntu.tar.gz --version 1

What you lose with WSL1: no Docker, no real Linux kernel, no systemd, weaker filesystem compatibility. Fine for CLI tooling, scripting, and build environments. If you need a real kernel, spin up a native Linux EC2 instance instead.