A portable Neovim + tmux + WezTerm command center for large-scale TypeScript/Node.js development with AI coding agents (Claude Code, avante.nvim) and a unified MCP toolset.
Runs on WSL2, native Linux desktops, headless Linux VPS (over SSH), and
macOS (with minor differences, all noted below). The repo is the single source of
truth: configs are symlinked into ~/.config, so editing the repo edits your live setup.
One-liner for a fresh VPS:
bash <(curl -sS https://raw.githubusercontent.com/dporkka/dotfiles/main/scripts/setup-workstation.sh) --yes --mode server --shell bash| Layer | Tools |
|---|---|
| Editor | Neovim 0.11+ / LazyVim · blink.cmp · fzf-lua · oil.nvim · conform · gitsigns + lazygit + diffview · harpoon · flash |
| AI in editor | avante.nvim (Cursor-style, <leader>a) · claudecode.nvim (<leader>k) · supermaven ghost-text · mcphub.nvim (MCP tools for avante) |
| AI in terminal | Claude Code CLI · git-worktree + tmux agent isolation · agent-session.sh (pings you when an agent finishes) |
| MCP | One blueprint (config/mcp/servers.json) → both Claude Code and avante. 6 core local servers: filesystem, memory, sequential-thinking, fetch, git, time |
| Multiplexer | tmux 3.x — seamless C-hjkl nav across nvim splits ↔ tmux panes (vim-tmux-navigator); tmux-resurrect + tmux-continuum |
| Terminal | WezTerm (local client) + EternalTerminal or Mosh for persistent remote sessions |
| Shell | bash or zsh + starship; modular ~/.bashrc.d/ fragments; secrets kept out of the repo |
| Ops | setup-workstation.sh, preflight.sh, dotfiles-doctor, dotfiles-info, newproj |
The bootstrap installs most of these on Debian/Ubuntu. On other distros / macOS, install the equivalents.
Core (all platforms):
- Neovim ≥ 0.11, tmux ≥ 3.2, git, zsh
- Node ≥ 18 (
nvmor system) — needed for npx MCP servers, mcphub, Claude Code uv/uvx— runs the Python MCP servers (curl -LsSf https://astral.sh/uv/install.sh | sh)- ripgrep (
rg),fd,fzf,jq - Claude Code CLI —
npm i -g @anthropic-ai/claude-code(or the installer) - A Nerd Font in your local terminal (CaskaydiaCove is the default) — for icons
Optional: lazygit, bat, eza, direnv, docker.
bash <(curl -sS https://raw.githubusercontent.com/dporkka/dotfiles/main/scripts/setup-workstation.sh) --yes --mode server --shell bashThis runs the full bundle: preflight checks, repo cloning, package installation, config symlinking, WezTerm setup, ET/Mosh server installation, and tmux persistence services. The installer is checkpointed — re-run the same command to resume from the last completed phase if it fails or is interrupted.
Options:
--mode desktop|server— desktop applies GNOME keyboard tweaks; server sets up ET/Mosh--shell bash|zsh— default login shell--with-runtimes— also install Node/Go/Python/Rust + LSPs (requires--shell zsh)--with-hardening— add swap, kernel tuning, SSH hardening, and fail2ban (server only)
# 1. Clone to ~/dotfiles (the path is assumed by the configs & scripts)
git clone https://github.com/dporkka/dotfiles ~/dotfiles
# 2. Bootstrap — installs packages, then SYMLINKS configs into ~/.config
# (idempotent; backs up any existing real files to *.bak.<timestamp>)
bash ~/dotfiles/scripts/bootstrap.sh
# 3. Add your secrets (NEVER committed — see "Secrets" below)
$EDITOR ~/.config/zsh/secrets.zsh # bootstrap seeds an empty template
# 4. Register the local MCP servers with Claude Code
bash ~/dotfiles/scripts/mcp-sync.sh
# 5. Reload + first-run plugin install
exec zsh
nvim --headless "+Lazy! sync" +qa # installs plugins + the mcp-hub binaryAlready have a clone and just want to (re)link configs without a full bootstrap:
bash ~/dotfiles/scripts/link-config.shThe symlink model:
~/.config/{nvim,tmux},~/.zshrc/~/.bashrc, and~/.config/starship.tomlare symlinks into this repo. Edit the repo, changes are live immediately — no copy/sync step. Live config follows the repo's checked-out branch, so keepmainchecked out for daily use.
AI agents consume significant RAM and CPU. Running many agents locally drains your battery, competes with your desktop environment, and often exceeds laptop memory. A dedicated VPS gives you:
- 24/7 compute — agents keep working while your laptop is closed or offline.
- Predictable cost — a 64 GB Contabo/OVH VPS costs ~$40–56/month flat; equivalent cloud resources cost 3–5× more.
- Isolation — experimental agent work stays on the server, not your primary machine.
- Better connectivity — wired datacenter networking beats home Wi-Fi for cloning, building, and pushing.
This repo is optimized for that workflow: install once on the VPS, connect from your local WezTerm over ET or Mosh, and let tmux + systemd keep sessions alive across disconnects and reboots.
This repo is also a Nix flake. It uses home-manager to install your daily tools and exposes reusable devShells for Go, Rust, Node/TypeScript, and Python projects.
- Reproducible. The exact same tools and versions install on every machine — your laptop, a VPS, WSL, or a fresh VM.
- Declarative. Your environment is code. Add a tool to
home.nixor a shell toshells.nix, run one command, and it’s there. - Isolated dev shells. Jump into a project and get only the toolchain it needs, without conflicting with your global packages or other projects.
- Atomic rollbacks. home-manager keeps generations. If an update breaks something, roll back in seconds.
- No more install scripts. Stop running
curl | bashor manually jugglingfnm,mise,rustup, and language servers. Nix provides them all. - Works alongside this repo. Your existing shell configs (
~/.zshrc,~/.bashrc, etc.) stay in place. The flake only adds packages.
-
Install Nix (single-user mode works on Fedora even with SELinux):
sudo mkdir -m 0755 /nix && sudo chown $USER /nix sh <(curl -L https://nixos.org/nix/install) --no-daemon . ~/.nix-profile/etc/profile.d/nix.sh
-
Enable flakes:
mkdir -p ~/.config/nix echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
-
Apply the home configuration:
cd ~/dotfiles nix run home-manager -- switch -b backup --flake .#$USER
-
Enter a dev shell:
nix develop ~/dotfiles#default # everyday CLI tools nix develop ~/dotfiles#go # Go + gopls + delve + golangci-lint nix develop ~/dotfiles#rust # Rust + cargo + clippy + rust-analyzer nix develop ~/dotfiles#node # Node + TypeScript + biome + prettierd nix develop ~/dotfiles#python # Python + uv + ruff + pyright + pytest nix develop ~/dotfiles#full # all of the above combined
- home-manager installs common packages:
git,just,tmux,tmuxp,fzf,zoxide,starship,eza,bat,ripgrep,fd,jq,yq-go,delta,lazygit,gh,direnv,nix-direnv,nixfmt,uv,nodejs,go,rustc,cargo,neovim,wget,curl,tree,htop,btop,unzip,zip. - Existing shell configs are preserved.
~/.bashrc,~/.zshrc,~/.profile, and~/.bash_profileare left untouched.direnvis installed but not auto-hooked, so addeval "$(direnv hook zsh)"(orbash) to your shell config if you want directory-local env loading. - Dev shells give you project-specific toolchains without polluting the global environment. Use them for the repos that don’t have their own flake (or as a quick fallback).
# Re-apply after editing home.nix or shells.nix
cd ~/dotfiles
nix run home-manager -- switch -b backup --flake .#$USER
# Check the flake for errors without building anything
nix flake check --no-build
# Update all flake inputs to their latest versions
nix flake update
# See home-manager generations and roll back if needed
home-manager generations
home-manager switch --flake .#$USER --generation <id>
home-manager switch --rollback
# Use with existing per-project flakes
# beads_viewer and bifrost already have their own flake.nix files.
# Enter them directly:
cd ~/beads_viewer && nix develop # Go dev shell for bv
cd ~/bifrost && nix develop # Bifrost dev shell- Add a global package: edit
home.nix, add the package name insidehome.packages, then re-runhome-manager switch. - Add a dev shell: edit
shells.nix, create a new package list and add it to the output set, thennix develop .#<name>. - Pin/unpin nixpkgs: change the
nixpkgs.urlinflake.nix. Runnix flake updateto regenerateflake.lock.
| File | Purpose |
|---|---|
flake.nix |
Flake inputs, home-manager configuration, dev shell outputs |
home.nix |
User packages and basic home-manager settings |
shells.nix |
default, go, rust, node, python, full dev shells |
flake.lock |
Locked dependency versions |
- Workstation Setup Bundle — one-shot installer for the full local/VPS environment.
- EternalTerminal + Mosh + Tmux Setup — persistent remote sessions over ET or Mosh with WezTerm, Tmux resurrection, and clipboard sync.
No secret ever lives in the repo. Real values go in ~/.config/zsh/secrets.zsh
(untracked, chmod 600), which ~/.zshrc sources:
# ~/.config/zsh/secrets.zsh
export ANTHROPIC_API_KEY="sk-ant-..." # avante.nvim + Claude
export TAVILY_API_KEY="tvly-..." # optional: avante @web searchANTHROPIC_API_KEYis required for avante (Claude Code uses its own auth).TAVILY_API_KEYis optional; avante's@websilently no-ops without it..gitignoreblocks**/secrets.zsh,*.local,.env*. If you ever paste a key into a tracked file by mistake, treat it as compromised and rotate it — git history is forever.
One file, config/mcp/servers.json, feeds both AI surfaces:
- Claude Code ←
scripts/mcp-sync.shregisters every server at user scope (claude mcp add-json --scope user). Re-run after editing the blueprint. - avante.nvim ←
mcphub.nvimreads the same file and exposes the tools to avante. Open the hub with:MCPHub(UI:Rrestart,gatoggle auto-approve,Mmarketplace).
The 6 default servers (official modelcontextprotocol/servers):
| Server | Runtime | Purpose |
|---|---|---|
| filesystem | npx (Node) | read/write under ~/dev |
| memory | npx (Node) | persistent knowledge graph |
| sequential-thinking | npx (Node) | structured reasoning |
| fetch | uvx (Python) | fetch & convert web pages |
| git | uvx (Python) | git ops (repo path per call) |
| time | uvx (Python) | time / timezone |
Verify: claude mcp list (all 6 should report Connected).
Add a server: edit config/mcp/servers.json, then bash ~/dotfiles/scripts/mcp-sync.sh
and :MCPHub → R in Neovim. Remote servers use {"type":"http","url":...}.
Change the filesystem scope: the blueprint uses ${HOME}/dev; edit it if your code lives elsewhere.
| Key | Action |
|---|---|
C-a | / C-a - |
Split vertical / horizontal |
Alt-\ / Alt-- |
Quick split vertical / horizontal (no prefix) |
Alt-n |
New pane (no prefix) · Alt-f floating scratch popup |
C-h/j/k/l |
Navigate panes and nvim splits (no prefix, seamless) |
C-a D / A / Q |
Dev / Agent (3-col) / Quad layout (now auto-runs lazygit/log panes) |
C-a a |
Agent dashboard — fzf every agent across sessions + live preview; Enter jumps |
C-a W |
Spawn agent in a new git worktree (prompts for branch) |
C-a f |
Project finder (fzf over ~/dev, etc.) |
C-a F |
Fuzzy session/window manager with live pane preview |
C-a g |
lazygit popup · C-a C-t scratch shell popup |
C-a S |
Session switcher · M-1..9 jump to window |
C-a C-r |
Reload config · C-a M-s/M-r resurrect save/restore · prefix + I/U TPM install/update |
C-a R |
Reconcile tmux agent registry after a mass restore |
C-a C-R |
Full manual restore: resurrect tmux sessions + reconcile + resurrect dead agents |
C-a C-c |
Copy last N lines of scrollback to /tmp/tmux-last-lines.txt |
C-a C-s |
Send a command to all panes in the current window |
C-a ! |
Manually mark current window as waiting-on-agent |
| Mode | Key | Actions inside mode |
|---|---|---|
| Pane | C-a p |
h/j/k/l move, |/- split, x close, z zoom, f float |
| Tab/Window | C-a t |
n new, c rename, x kill, h/l prev/next, 1-9 jump, ,/. move |
| Resize | C-a r |
h/j/k/l ±5, H/J/K/L ±10, m zoom |
| Session | C-a o |
s switch, n new, f finder, d detach, x kill, $ rename |
AI — Claude Code (<leader>k)
| Key | Action |
|---|---|
<leader>kk |
Toggle Claude Code |
<leader>ks |
Send visual selection as context |
<leader>kf / <leader>kb |
Focus / add current buffer |
<leader>kr / <leader>kC |
Resume / continue session |
<leader>ka / <leader>kd |
Accept / deny a proposed diff (:w/:q also work) |
<leader>kc |
Copy AST context block (file + fenced code) to clipboard |
AI — avante (<leader>a)
| Key | Action |
|---|---|
<leader>aa / <leader>ae |
Ask / edit selection |
<leader>at |
Toggle sidebar · <leader>aM repomap |
<leader>ac / <leader>ao |
Switch to Sonnet / Opus |
@ (in sidebar) |
Add file/symbol to context |
<Tab> (insert) |
Accept supermaven ghost suggestion |
MCP: :MCPHub · :checkhealth mcphub
Navigation / Git / LSP / Terminal (unchanged highlights):
Space Space files · Space / grep · - oil · s flash · M-1..4 harpoon ·
gd/gr/K LSP · Space ca/Space rn/Space cf · ]h/[h + Space ghs hunks ·
Space gg lazygit · Space gd diffview · Space gm review branch vs main · C-` float term.
# Jump to / create a tmux session for a project
work ~/dev/myproject
# One command: git worktree + tmux session (agent/editor/review) + launch agent.
# Each agent gets its own branch & dir, so parallel agents never collide.
agent-worktree.sh feat/payments "add MFA to the login form" # prompt is optional
# ↳ also bound to C-a W (prompts for a branch). --base <b> / --agent <cmd> to override.
# Just a worktree, no agent: new-worktree.sh feat/x main
# Dedicated agent session (editor/watch/review), pings on process exit:
agent-session.sh refactor-auth claudeMission control — which agent needs me?
C-a a→ fzf dashboard of every agent window across all sessions, with a live pane preview; Enter jumps to it.- Status bar shows
⚡N waiting / ✓N doneacross all sessions; window tabs get ⚡/✓ glyphs. - This state comes from Claude Code hooks (
Stop/Notification/UserPromptSubmit→scripts/agent-hook.sh→ tmux@agent_state), which also fire the desktop/WSL toast.doneonly toasts when you're not looking at that pane, so active pairing stays quiet. ⚠️ The hooks live in~/.claude/settings.json— machine-local, NOT in this repo — so re-add them after cloning onto a new machine.
Persistence & resurrection
tmux.servicestarts on login and keeps the tmux server alive so sessions survive terminal closure, sleep, and reboot.tmux-snapshot.timersnapshots the agent registry every 2 minutes to~/.local/state/agents/snapshots/.tmux-resurrect+tmux-continuumsave tmux sessions every 15 minutes and auto-restore them when the tmux server starts.- After a restore, dead agent sessions are automatically resurrected from the latest registry snapshot.
- Manual controls:
C-a C-R— full manual restore (resurrect tmux sessions + reconcile registry + resurrect dead agents)C-a R— reconcile registry after a mass restoresystemctl --user {start|stop|status} tmux.serviceagent-registry.sh snapshot/agent-registry.sh restore latestagent-resurrect.sh all --dry-run/agent-resurrect.sh all
- A shell hook (
scripts/agent-shell-hook.sh) runs on everyprecmd/chpwdto keep registry worktree/branch metadata in sync. SetAGENT_AUTO_RESURRECT=trueto automatically revive a dead agent session when youcdback into its worktree.
Review each agent's work in Neovim with Space gm → diffview of main...HEAD
(the whole branch diff), or open its review window.
Always develop in ~/dev, never /mnt/c/. The 9P filesystem is catastrophically slow
for metadata-heavy work:
| Operation | ~/dev (ext4) |
/mnt/c/ (9P) |
|---|---|---|
git status |
~50ms | ~5,000ms |
pnpm install |
~30s | ~5min+ |
| HMR / inotify | native | broken |
.wslconfig(C:\Users\<you>\.wslconfig): memory cap,autoMemoryReclaim=gradual,networkingMode=mirrored,sparseVhd=true. Seewsl/.wslconfig. Apply:wsl --shutdown.- inotify limits (bootstrap sets these):
fs.inotify.max_user_watches=524288— the default 8192 breaks Next.js/Vite HMR. - Clipboard:
clip.exe/win32yank.exebridge to Windows (tmux & nvim auto-detect WSL). - Node-on-PATH (important):
node/npxfromnvmare lazy shell-functions absent from non-interactive PATH, so Claude Code /mcp-hubwould otherwise fall back to Windows node and break MCP (a UNC banner corrupts the stdio stream). Bootstrap symlinks the Linux toolchain into~/.local/bin(ahead of Windows node on PATH) to fix this. Re-run after a node version change:for b in node npx npm mcp-hub; do ln -sf "$(ls -d ~/.nvm/versions/node/*/bin|tail -1)/$b" ~/.local/bin/$b; done
- Skip all WSL steps; on a headless server you connect with WezTerm + ET or Mosh, then run Tmux on the server.
- Install Node (nvm or
apt/dnf),uv(for uvx MCP servers), ripgrep, fd, fzf, jq, Claude Code. System node is on PATH already, so the MCP blueprint works as-is. - Set up the network layer on the server:
- ET:
scp ~/dotfiles/scripts/setup-et-server.sh server:/tmp/ && ssh server 'bash /tmp/setup-et-server.sh' - Mosh:
scp ~/dotfiles/scripts/setup-mosh-server.sh server:/tmp/ && ssh server 'bash /tmp/setup-mosh-server.sh'
- ET:
- Clipboard: rely on OSC52 — tmux has
set -g set-clipboard on; use a terminal that supports OSC52 (WezTerm does) so yanks reach your local clipboard. - Persistent sessions:
- ET:
et server -c "tmux new-session -A -s main"orLEADER + e/LEADER + E - Mosh:
mosh server -- tmux new-session -A -s mainorLEADER + m/LEADER + M
- ET:
- Desktop notifications degrade gracefully:
notify.shfalls back to the terminal bell when no GUI /notify-sendis present.
- Install deps with Homebrew (
brew install neovim tmux ripgrep fd fzf jq uv). - Clipboard works natively (
pbcopy/pbpaste); the WSLclip.exealias is harmless/unused. - Skip WSL-specific steps. Everything else is identical.
A daily rclone backup of $HOME is configured via systemd.
# One-time Google Drive OAuth setup
rclone config
# name: gdrive
# storage: Google Drive
# scope: 1 (Full access)
# use auto config: y (opens browser)
# Dry-run to preview what would be uploaded
backup-dry
# Run a backup now
backup-now
# Check timer status
backup-timerFiles excluded from the backup: caches (~/.cache, node_modules, etc.), build
artifacts, secrets (SSH private keys, secrets.zsh, .env*), and files larger
than 100 MiB. Overwritten or deleted files are moved to a timestamped
home-versions/ folder on Google Drive so you can recover older copies.
The systemd timer runs once per day with a 30-minute randomized delay:
systemctl --user status backup-home-gdrive.timer.
# Edit a config — it's a symlink, so changes are live immediately. Then:
sync-dotfiles.sh "msg" # commit + push the repo
# Health and status
dotfiles-doctor # verify symlinks, binaries, plugins, systemd units
dotfiles-info # quick cached overview of sessions and agent state
# Update plugins / tools
nvim --headless "+Lazy! update" +qa
bash ~/dotfiles/scripts/mcp-sync.sh # after editing the MCP blueprint
# Project scaffolding
newproj myapp --stack node --tmuxp --claude
# Health
nvim --startuptime /tmp/nvim.log && tail -3 /tmp/nvim.log # target <100ms
claude mcp list # MCP server health
# inside nvim: :checkhealth · :checkhealth mcphub · :LspInfo · :MasonMCP server "Failed to connect" — almost always node-on-PATH:
command -v npx # must be a Linux/native npx (NOT /mnt/c/... on WSL)
claude mcp get filesystemOn WSL, ensure ~/.local/bin/{node,npx} symlinks exist (see WSL notes). uvx servers
(fetch/git/time) failing instead means uv isn't installed.
Neovim clipboard not working (WSL) — win32yank.exe -o should echo back; re-run bootstrap.
inotify ENOSPC — sudo sysctl fs.inotify.max_user_watches=524288.
LSP not starting — :LspInfo, :Mason (install the server), :LspLog.
tmux plugins missing — git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm, then prefix + I.
<C-hjkl> doesn't cross into tmux — needs vim-tmux-navigator (installed by this config) and
the matching is_vim bindings in tmux.conf (already present). Reload tmux: prefix + r.
A config change didn't take effect — confirm it's symlinked: readlink ~/.config/nvim
should point into ~/dotfiles. If not, run link-config.sh.