Keep Claude Code running 24/7 per project in screen (with Remote Control) plus a Neovim server, as systemd user services; attach over SSH with one command
  • Go 99.2%
  • Shell 0.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
R2D2 ebbde9ab48
All checks were successful
ci/crow/push/test/1 Pipeline was successful
ci/crow/push/test/2 Pipeline was successful
ci/crow/tag/test/1 Pipeline was successful
ci/crow/tag/test/2 Pipeline was successful
ci/crow/tag/release Pipeline was successful
Release notes: a release compares with the previous release
Skip pre-release tags when finding the previous tag for a release, so
v0.3.0's notes cover everything since v0.2.1 rather than since its last
release candidate. Pre-releases still compare with whatever came before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yRhdKoAN89JPCt5DDfRYA
2026-10-30 12:47:27 -07:00
.crow CI: let git read the checkout in the Go containers 2026-10-23 11:58:10 -07:00
scripts Release notes: a release compares with the previous release 2026-10-30 12:47:27 -07:00
skills/claude-wrangler ws start|stop|restart --all [<host>]: every workspace on a machine 2026-10-30 12:28:17 -07:00
.gitignore Ignore .env 2026-10-23 11:31:40 -07:00
AGENTS.md Add AGENTS.md: the update contract every release must keep 2026-10-23 15:54:32 -07:00
all.go ws start|stop|restart --all [<host>]: every workspace on a machine 2026-10-30 12:28:17 -07:00
all_test.go ws start|stop|restart --all [<host>]: every workspace on a machine 2026-10-30 12:28:17 -07:00
clipboard.go Make nvim, the paste key and screen's TERM configurable 2026-10-22 17:33:29 -07:00
clipboard_test.go Make nvim, the paste key and screen's TERM configurable 2026-10-22 17:33:29 -07:00
completion.go ws start|stop|restart --all [<host>]: every workspace on a machine 2026-10-30 12:28:17 -07:00
completion_test.go Group workspace commands under workspace (ws), with targets and help 2026-10-23 15:50:58 -07:00
config.go Make nvim, the paste key and screen's TERM configurable 2026-10-22 17:33:29 -07:00
connect.go update host: work with any older claude-wrangler there 2026-10-23 15:54:32 -07:00
connect_test.go Use generic names in tests 2026-10-22 16:57:39 -07:00
doctor.go Group workspace commands under workspace (ws), with targets and help 2026-10-23 15:50:58 -07:00
doctor_test.go Group workspace commands under workspace (ws), with targets and help 2026-10-23 15:50:58 -07:00
env.go Use $HOME as the home directory, like Claude Code does 2026-10-23 15:07:47 -07:00
go.mod Require Go 1.22, not 1.27 2026-10-22 16:57:39 -07:00
help.go ws start|stop|restart --all [<host>]: every workspace on a machine 2026-10-30 12:28:17 -07:00
helpers_test.go ws start|stop|restart --all [<host>]: every workspace on a machine 2026-10-30 12:28:17 -07:00
LICENSE.md Add the MIT license 2026-10-22 14:55:17 -07:00
local.go Group workspace commands under workspace (ws), with targets and help 2026-10-23 15:50:58 -07:00
local_test.go Connect to a workspace on this machine with connect/nvim/shell :<name> 2026-10-22 14:11:36 -07:00
main.go ws start|stop|restart --all [<host>]: every workspace on a machine 2026-10-30 12:28:17 -07:00
main_test.go Group workspace commands under workspace (ws), with targets and help 2026-10-23 15:50:58 -07:00
README.md ws start|stop|restart --all [<host>]: every workspace on a machine 2026-10-30 12:28:17 -07:00
settings_test.go Make nvim, the paste key and screen's TERM configurable 2026-10-22 17:33:29 -07:00
skill.go Add a Claude Code skill that explains workspaces, and install-skill 2026-10-21 07:43:18 -07:00
skill_test.go Group workspace commands under workspace (ws), with targets and help 2026-10-23 15:50:58 -07:00
target.go Group workspace commands under workspace (ws), with targets and help 2026-10-23 15:50:58 -07:00
trust.go claude-wrangler: Go rewrite of the workspaces.nvim tooling 2026-10-19 15:05:14 -07:00
trust_test.go claude-wrangler: Go rewrite of the workspaces.nvim tooling 2026-10-19 15:05:14 -07:00
update.go update host: work with any older claude-wrangler there 2026-10-23 15:54:32 -07:00
update_test.go update host: work with any older claude-wrangler there 2026-10-23 15:54:32 -07:00
version.go Add doctor and version commands 2026-10-22 21:13:17 -07:00
version_test.go Add doctor and version commands 2026-10-22 21:13:17 -07:00
workspace.go ws start|stop|restart --all [<host>]: every workspace on a machine 2026-10-30 12:28:17 -07:00
workspace_test.go Group workspace commands under workspace (ws), with targets and help 2026-10-23 15:50:58 -07:00

claude-wrangler

Keep a Claude Code session running 24/7 for each of your projects, reachable from your phone, and attach to it from your laptop with one command.

A workspace is a project directory with two systemd user services:

  • Claude: a screen session named cw_<workspace> whose only window runs Claude Code with --remote-control, so you can pick it up from the Claude app or claude.ai. It continues the directory's last conversation, and if Claude ever exits, systemd starts it again a few seconds later.
  • Neovim (optional, on by default): a headless nvim server in the same directory, for attaching an editor from another machine.

Both start at boot (with lingering) and keep running while you're logged out.

Quick start

On the machine that will run your workspaces (Linux with systemd), after installing claude-wrangler and logging in to Claude Code (claude auth login):

loginctl enable-linger                  # keep workspaces running when you log out
claude-wrangler doctor                  # check everything they need is in place
claude-wrangler ws create :notes        # ~/Projects/workspaces/notes; asks about trust
claude-wrangler ws start :notes         # start it now and at every boot
claude-wrangler ws connect :notes       # attach to Claude; Ctrl-a d detaches

From your laptop, with claude-wrangler installed there too:

claude-wrangler ws connect you@server:notes   # the same Claude, over ssh
claude-wrangler ws connect you@server         # or pick from a menu

On your phone, open the Claude app or claude.ai and pick the Remote Control session cw_notes. ws is short for workspace; with the recommended alias all this is cw ws ….

Requirements

The machine that runs the workspaces (the server):

  • Linux with systemd, and a systemd user session for your account. Lingering (loginctl enable-linger) keeps workspaces running while you're logged out and starts them at boot. macOS can't host workspaces (no systemd); WSL can only if systemd is turned on there.
  • GNU screen (developed with 5.0; any recent version with -U, -T and bindkey … exec should do).
  • Claude Code, logged in with a claude.ai account (claude auth login). Remote Control needs a claude.ai login, not an API key, and a plan that includes Remote Control. claude-wrangler relies on a few Claude Code behaviours; see Claude Code compatibility.
  • Neovim 0.9 or later, unless you turn the nvim servers off ("nvim": false, see Configuration).
  • sshd, to connect from other machines.

A machine you connect from (your laptop):

  • Linux or macOS with OpenSSH (6.7 or later, for forwarding Unix sockets). Windows isn't supported, except possibly through WSL, which is untested.
  • Neovim 0.9 or later for claude-wrangler ws nvim.
  • For pasting images (Ctrl+V): wl-paste (wl-clipboard) on Wayland, xclip on X11, or pngpaste on macOS.
  • claude-wrangler itself, ideally the same version as the server's. Even that is optional for Claude: ssh -t user@host screen -U -d -r cw_<name> attaches without it.

claude-wrangler doctor checks all of this on the machine you run it on.

Install

Install claude-wrangler on the machine that runs your workspaces and on each machine you connect from. Step 1 is needed everywhere and step 2 on the server; the rest is recommended.

1. Get the binary

From a release:

Download the binary for your system from the releases page, check it against SHA256SUMS, and put it on your PATH:

v=v0.1.0
os=$(uname -s | tr '[:upper:]' '[:lower:]')   # linux or darwin
case $(uname -m) in x86_64|amd64) arch=amd64 ;; aarch64|arm64) arch=arm64 ;; armv6l|armv7l) arch=arm ;; esac
file=claude-wrangler-$os-$arch
base=https://git.jshaver.net/jshaver/claude-wrangler/releases/download/$v
curl -fsSLO "$base/$file" && curl -fsSLO "$base/SHA256SUMS"
grep " $file\$" SHA256SUMS | sha256sum -c         # macOS: shasum -a 256 -c
mkdir -p ~/.local/bin && install -m 755 "$file" ~/.local/bin/claude-wrangler
File For
claude-wrangler-linux-amd64 Linux on Intel/AMD
claude-wrangler-linux-arm64 64-bit ARM Linux (Raspberry Pi 3/4/5 with a 64-bit OS, ARM servers)
claude-wrangler-linux-arm 32-bit ARM Linux (ARMv6 and later: every Raspberry Pi on a 32-bit OS)
claude-wrangler-darwin-arm64 Macs with Apple silicon (M1 and later)
claude-wrangler-darwin-amd64 Intel Macs

~/.local/bin is also where connect, nvim and shell look for claude-wrangler on the other machine, along with ~/go/bin. On a Mac, a binary downloaded with a browser instead of curl is quarantined by Gatekeeper because it isn't signed by Apple; clear that once with xattr -d com.apple.quarantine ~/.local/bin/claude-wrangler.

Or with Go:

go install git.jshaver.net/jshaver/claude-wrangler@latest   # or, from a clone: go install .

The binary lands in $(go env GOPATH)/bin (usually ~/go/bin). Building needs Go 1.22 or later and only the standard library.

2. On the server: keep workspaces running

To keep workspaces running while you're logged out, and to start them at boot, enable lingering once on the machine that runs them:

loginctl enable-linger

claude-wrangler doctor then checks the rest of the requirements.

eval "$(claude-wrangler completion zsh)"    # in ~/.zshrc, after compinit
eval "$(claude-wrangler completion bash)"   # in ~/.bashrc

(For zsh you can instead save the script as _claude-wrangler in a $fpath directory.) It completes subcommands, flags, your workspace names, and hosts from ~/.ssh/config for the remote commands.

claude-wrangler is a lot to type for every command. We recommend an alias, cw, with completion hooked up to match:

# ~/.zshrc, after the completion line above
alias cw=claude-wrangler
compdef cw=claude-wrangler

# ~/.bashrc, after the completion line above
alias cw=claude-wrangler
complete -F _claude_wrangler cw

Then cw ws connect hub:notes, cw ws list, cw doctor and so on. This README spells out claude-wrangler so its commands work with or without the alias. Only your shell knows about it: anything that runs claude-wrangler for you (screen's paste key, connect on the other machine) uses the full name.

claude-wrangler install-skill

installs a skill (built into the binary) in ~/.claude/skills/claude-wrangler/ that teaches Claude how workspaces work, so you can ask it things like "how do I disconnect and keep you running?" (Ctrl-a d) or use /claude-wrangler. Install it on the machine that runs the workspaces; claude-wrangler update refreshes it, and after any other kind of upgrade, rerun install-skill. The source is skills/claude-wrangler/SKILL.md.

Updating

claude-wrangler update              # this machine
claude-wrangler update you@server   # another machine, over SSH

update asks Forgejo for the latest release (never a pre-release), and if it's newer than what you have, downloads the binary for your machine, checks it against the release's SHA256SUMS, and puts it in place of the one you're running. It prints the version you had and the one you have now, and refreshes the Claude Code skill if it's installed. Running workspaces carry on; the next Ctrl+V already uses the new binary.

  • Pre-releases (tags like v0.3.0-rc.1) are only installed when you ask, with claude-wrangler update --pre. That applies to that one run: a later plain update ignores pre-releases again, so on v0.3.0-rc.1 you stay put until v0.3.0 (or anything newer) is released. To keep testing pre-releases, use --pre each time.
  • A new major version (v1 to v2, say) may change how things work, so update asks first, defaulting to no. --yes skips the question.
  • A build that isn't a release (from a clone, or go install of a commit, which version shows as dev-… or v0.0.0-…) isn't replaced unless you add --force, so a binary you built yourself doesn't get overwritten by surprise.
  • On another machine, claude-wrangler must already be installed for that user, in a version that has update; otherwise it says so and changes nothing. Its output, including any question, comes through your terminal.
  • The release files must be readable without logging in, which they are once the repository is public. For a private copy, put a Forgejo token with read access in $CLAUDE_WRANGLER_TOKEN.

Usage

claude-wrangler workspace <command> [flags] [<target>]   (ws for short)
claude-wrangler update | doctor | install-skill | completion | version | help

claude-wrangler help lists everything, and claude-wrangler help <command> (or <command> --help, ws <command> --help) explains one in detail.

Targets

Every workspace command takes the same last argument, saying which workspace on which machine:

Target Means
(nothing) pick from this machine's workspaces
:notes workspace notes on this machine
hub or hub: pick from hub's workspaces
hub:notes workspace notes on hub

hub can be user@hub or any host from ~/.ssh/config. A bare word is always a host, as with ssh: claude-wrangler ws start notes means the machine called notes. If that isn't reachable but there's a workspace of that name here, the error says to write :notes.

On another machine the command runs there, over ssh, as claude-wrangler workspace <command> … :<name>, with your terminal attached for its questions and output. So claude-wrangler must be installed there for the user you log in as: workspaces belong to the user that runs them. Every step of one command shares a single ssh connection (a ControlMaster), so you authenticate once and your ~/.ssh/config applies.

Connecting

claude-wrangler ws connect [<target>]   # attach to the workspace's Claude
claude-wrangler ws nvim [<target>]      # attach an nvim UI to its nvim server
claude-wrangler ws shell [<target>]     # open a shell in its project directory
Command What it does
connect Attaches to the workspace's screen session (screen -U -d -r cw_<name>, which detaches it from any other terminal). In it, Ctrl-a d detaches and leaves Claude running, Ctrl-a Esc scrolls back, and Ctrl+V pastes an image from the machine you're on. With no name, the menu also offers n to create a workspace (asking for its name and about trust) and start it.
nvim Runs nvim --remote-ui against the workspace's nvim server; for another machine its socket is forwarded over ssh. :detach, or closing the terminal, leaves the server running.
shell Opens a login shell in the project directory. The workspace doesn't need to be running.

connect and nvim offer to rebuild a workspace set up by an older claude-wrangler and to start a stopped one, and wait up to 15 seconds for one that's restarting. On this machine connect refuses to attach a workspace's screen session to itself (from its own Ctrl-a c shell).

Quitting Claude inside screen (Ctrl-C twice, /exit) ends the session, and systemd starts it again about 5 seconds later with --continue, so the conversation carries on. To leave without that restart, detach with Ctrl-a d. The same goes for :q in an attached nvim UI.

Pasting images from your laptop

Claude Code's own image paste reads the clipboard of the machine Claude runs on, and a terminal can only send text over ssh. So in a workspace attached from another machine with claude-wrangler ws connect, Ctrl+V pastes the image on your laptop's clipboard instead (change or turn off the key with paste_key, see Configuration):

  1. The workspace's screen session catches Ctrl+V (its own screenrc; other screen sessions are untouched) and starts a background fetch, so you can keep typing.
  2. It asks your laptop, over the connection connect already has (a reverse socket forward), for the clipboard image. Nothing leaves your laptop until you press Ctrl+V.
  3. The image is saved, unchanged, as ~/.claude-wrangler/clipboard/<name>/<hash>.<ext> on the server (only the latest is kept), and its path is typed into Claude's input. Claude can read that directory without asking (--add-dir).

Progress shows on screen's message line ("Fetching the image on your clipboard...", "Inserted ...", "No image on your clipboard", or why it failed; it gives up after 30 seconds). Your laptop needs wl-paste (wl-clipboard, Wayland), xclip (X11) or pngpaste (macOS). In other screen windows (Ctrl-a c), and when attached without a remote connect (plain ssh, or connect :<name> on the server itself), Ctrl+V goes to Claude as usual, which reads the server's own clipboard.

Managing workspaces

claude-wrangler ws list [--names|--running|--outdated] [<target>]
claude-wrangler ws create [--trust|--no-trust] [<target>]
claude-wrangler ws start|stop|restart [<target>]
claude-wrangler ws start|stop|restart --all [<host>]
claude-wrangler ws upgrade [<target>]
claude-wrangler ws upgrade --all [<host>]
claude-wrangler ws destroy [-y] [<target>]
claude-wrangler ws dir [<target>]
Command What it does
list One line per workspace: its Claude service, whether its cw_ screen session exists, its nvim service, whether it starts at boot, and whether its services are current or outdated. With a workspace (:notes, hub:notes), just that one; without one it lists them all rather than asking. --names prints only the names; --running, those whose screen session exists; --outdated, those needing a rebuild.
create Writes the workspace's services and, if it doesn't exist yet, its project directory ({workspaces_dir}/{name}). An existing directory is used as is; nothing in it is changed. Without a name it asks for one. Refuses a name whose cw_ screen session is already running. For --trust/--no-trust, see Claude Code trust.
start Starts the workspace (Claude, and nvim through Wants=) and enables it, so it also comes back after a reboot.
stop Stops both services and disables the workspace, so it stays down across reboots.
restart Restarts both services.
--all With start, stop or restart: every workspace on this machine, or on <host>, one after another, printing each one's state before and after. One that fails doesn't stop the rest. stop --all and then start --all pause everything, for example while updating. Run from inside a workspace, stop --all and restart --all do that workspace last, since doing it ends the session you're in.
upgrade Rebuilds an outdated workspace's services; --all does every outdated one on this machine, or on <host>. See Upgrading.
destroy Asks (unless -y), then removes both services. The project directory is kept.
dir Prints the project directory, e.g. cd "$(claude-wrangler ws dir :notes)".

Names may contain letters, digits, - and _. Screen sessions and Claude's session and Remote Control names get a cw_ prefix (cw_notes), so they don't collide with sessions you start by hand. create and start print the commands to reach the workspace from another machine. Commands exit non-zero on failure and print errors to stderr.

claude-wrangler itself

Command What it does
update [--pre] [--force] [--yes] [[user@]host] Installs the latest release here, or on another machine; see Updating.
doctor Checks everything workspaces depend on and says how to fix what's wrong; see Doctor. Exits 1 if something is broken.
install-skill Installs the Claude Code skill.
completion zsh|bash Prints the shell completion script; see shell completion.
version Prints the version (--version works too).
help [<command>] Explains a command; help workspace <command> for workspace commands.

Doctor

claude-wrangler doctor changes nothing; it reports ok, warn, FAIL (and exits 1) or - (just so you know) for:

  • this machine as a host: the systemd user session, lingering, screen, nvim (0.9 or later, unless turned off), Claude Code, that it supports Remote Control and is logged in with a claude.ai account (Remote Control needs one; claude auth status), and whether the skill is current;
  • each workspace: outdated or still on the old workspaces.nvim layout, enabled but not running, a service without its screen session, a missing nvim server, and Claude waiting at "Do you trust this folder?" (read from the screen window), which keeps Remote Control from connecting;
  • connecting out: ssh, nvim for the nvim command, and the clipboard tool image paste needs on a desktop.

Configuration

Settings are read from ~/.config/claude-wrangler/config.json ($XDG_CONFIG_HOME is honoured, and $CLAUDE_WRANGLER_CONFIG overrides the path). The file is optional, every field has a default, and unknown fields are rejected so typos don't go unnoticed:

{
  "workspaces_dir": "~/Projects/workspaces",
  "ssh_hostname": "example.com",
  "ask_claude_trust": true,
  "nvim": false,
  "paste_key": "^[v"
}
Field Default Meaning
workspaces_dir ~/Projects/workspaces Where project directories are created.
ssh_hostname this machine's hostname Host shown in the printed connect commands.
command see below Shell command run in the screen session. {name} becomes the workspace name, {session} the prefixed session name (cw_<name>), {claude} the claude binary and {clipboard} the pasted-images directory.
ask_claude_trust true Whether create asks about Claude Code trust when neither switch is given.
nvim true Whether each workspace gets a headless nvim server. With false there's no nvim unit, list shows off in the NVIM column, and claude-wrangler ws nvim says it's turned off. nvim only needs to be installed when this is on.
paste_key "^V" The key that pastes an image from your laptop (see Pasting images), in screen's caret notation: "^V" is Ctrl+V, "^[v" is Alt-v (Escape, v). It must start with a control character. "off" leaves every key to Claude. On the machine you connect from, "off" stops connect forwarding the clipboard socket.
screen_term "screen-256color" $TERM inside the screen session (screen -T). Try "screen" if a host lacks the 256-colour terminfo entry.

The default command is

{claude} --continue --name {session} --remote-control {session} --add-dir {clipboard} || exec {claude} --name {session} --remote-control {session} --add-dir {clipboard}

It continues the directory's last conversation; the first time there is none, --continue exits with an error straight away and a new session starts. Both the session name (/resume) and the Remote Control name are cw_<workspace>.

Settings are baked into a workspace's units when it's created or rebuilt. Changing nvim, paste_key or screen_term makes existing workspaces show as outdated, and connect offers the rebuild, as does claude-wrangler ws upgrade <target> (or --all). Changing command or workspaces_dir doesn't flag them; run claude-wrangler ws upgrade yourself to apply command to an existing workspace.

Claude Code trust

Claude Code asks "Do you trust this folder?" the first time it runs in a directory, and won't connect to Remote Control until someone answers. claude-wrangler ws create can answer it for you: it asks whether Claude Code should trust the new workspace directory (with ask_claude_trust), or takes --trust / --no-trust to answer in advance. Without a terminal to ask on, the answer is no; you can still answer Claude's own prompt later with connect.

Answering yes sets projects[<dir>].hasTrustDialogAccepted in ~/.claude.json, exactly what Claude Code's own "Yes" writes. claude-wrangler holds the lock Claude Code uses while saving that file (the ~/.claude.json.lock directory), re-reads the file under it, changes only that one entry (every other value is kept byte for byte), and replaces the file atomically, keeping it private (0600). If the lock is held for more than 5 seconds it gives up rather than risk losing a write.

How a workspace is set up

For a workspace called notes:

project directory ~/Projects/workspaces/notes
Claude unit ~/.config/systemd/user/workspace-notes.service, enabled into default.target.wants/
screen session cw_notes (screen -ls), started with screen -D -m -U -T screen-256color -c <screenrc> -S cw_notes /bin/sh -c <command>
screenrc ~/.claude-wrangler/screen/notes.screenrc: sources your ~/.screenrc (if any) and binds Ctrl+V (paste_key) to image paste
pasted images ~/.claude-wrangler/clipboard/notes/
Neovim unit ~/.config/systemd/user/nvim-workspace-notes.service, pulled in by the Claude unit's Wants= (not there with "nvim": false)
Neovim socket $XDG_RUNTIME_DIR/nvim-workspace-notes.sock

Inside screen, Claude runs with CLAUDE_CODE_DISABLE_MOUSE=1 and TERM=screen-256color. Claude otherwise asks the terminal to report every mouse movement in a format screen can't decode, and the reports leak into its input as text like 119;12M118;9M; use screen's scrollback (Ctrl-a Esc) instead of the mouse wheel. Attaching uses screen -U, so non-ASCII characters show correctly even when the SSH session has no LANG.

Both units restart on any exit (Restart=always, 5 s) and treat exit status 1 as success, since screen and nvim exit 1 when systemd stops them. They are deliberately not bound with PartOf=: that would restart Neovim every time Claude restarts, so stop, restart and destroy act on both units instead. The nvim unit removes stale sockets before each start and after each stop.

Upgrading

Both units carry an X-ClaudeWrangler-Layout=<n> line (systemd ignores X- keys). When a new claude-wrangler changes how units are written, it bumps that number, and workspaces whose units have an older number, no number (anything from before this marker, including workspaces.nvim's single nvim unit), or a missing nvim unit count as outdated. So do workspaces built with other nvim, paste_key or screen_term settings than the current config: the Claude unit lists any non-default ones in an X-ClaudeWrangler-Settings= line. list shows them, and:

  • start and restart offer to rebuild an outdated workspace first (default yes). A workspaces.nvim unit can't start until it's rebuilt.
  • connect and nvim ask the machine that runs the workspace (claude-wrangler ws list --outdated there) and offer to run claude-wrangler ws upgrade there, so it's that machine's installed version that decides, not yours.
  • claude-wrangler ws upgrade <target> rebuilds one without asking, and claude-wrangler ws upgrade --all [<host>] every outdated one.

Rebuilding rewrites both units from the installed version and current config and reloads systemd. The project directory and Claude Code's trust are left alone, and whether the workspace starts at boot is kept. A running workspace is restarted (Claude continues its last conversation); a stopped one stays stopped.

After updating claude-wrangler, run claude-wrangler ws upgrade --all (or … --all <host> for another machine) to bring existing workspaces up to date; connect also offers it workspace by workspace.

An old startup_commands setting is rejected with a pointer to command.

Claude Code compatibility

claude-wrangler drives Claude Code from the outside, so it depends on some of its behaviour that isn't a stable interface. It was last checked with Claude Code 2.1.309. If a Claude Code update breaks something, this is where to look:

Relies on Used for If it changes
claude --continue exiting with an error straight away when the directory has no conversation The default command falls back to a new session (… || exec claude …) A new workspace's first start fails or loops; adjust command.
--name <name> and --remote-control <name> Naming the session and the Remote Control entry cw_<name> The phone shows a generic name, or Remote Control doesn't start.
--add-dir <dir> Letting Claude read pasted images without asking Claude asks before opening each pasted image.
CLAUDE_CODE_DISABLE_MOUSE=1 Stopping mouse reporting that screen can't pass through Text like 119;12M appears in Claude's input.
~/.claude.json: projects[<dir>].hasTrustDialogAccepted, written under the ~/.claude.json.lock directory lock create --trust --trust has no effect and Claude asks "Do you trust this folder?" anyway; doctor reports it. A file it can't parse is left alone.
The trust question's wording, "Yes, I trust this folder" doctor spotting a workspace stuck at that question doctor stops noticing it.
claude auth status --json (loggedIn, authMethod, subscriptionType) doctor checking for a claude.ai login doctor says it couldn't check the login.
claude --help listing --remote-control doctor checking for Remote Control support doctor reports it missing.

claude-wrangler doctor exercises most of these; run it after updating Claude Code.

Security notes

A workspace is Claude Code running unattended, around the clock, as your user. Before you rely on it:

  • Remote Control lets your claude.ai account drive it. Anyone who can use your claude.ai account (from the app or the web) can send prompts to every workspace and answer Claude's permission prompts, which means running commands and editing files on the server as you. Protect the account accordingly, with a strong password and two-step verification.
  • claude-wrangler doesn't loosen Claude's permissions. Claude asks before running commands or editing files exactly as it would in a terminal. The command setting lets you add flags such as --dangerously-skip-permissions; on an always-on workspace reachable from your phone, think twice before doing that.
  • Trusting a directory trusts its contents. create --trust answers Claude Code's "Do you trust this folder?" for you, by editing ~/.claude.json. A trusted project's own Claude settings (hooks, MCP servers) then take effect, so only trust directories whose contents you trust, as you would when Claude asks.
  • No ports are opened. Everything goes over SSH or through Unix sockets that only your user can reach: screen's session socket, each nvim server's socket in $XDG_RUNTIME_DIR, and the clipboard socket below. Anyone who can run code as your user on the server can use them, which already means they could do anything you can.
  • Image paste reads your laptop's clipboard only when asked, and only for images. Nothing leaves your laptop until you press the paste key in Claude's window; claude-wrangler never reads text from the clipboard. While connect is attached, though, the forwarded clipboard socket ($XDG_RUNTIME_DIR/claude-wrangler/clipboard-<name>.sock) answers any process running as your user on the server, including Claude itself, so a process there could fetch whatever image is on your laptop's clipboard at that moment. If that matters to you, set "paste_key": "off" in the config on your laptop: connect then doesn't forward the socket at all. (Set it on the server too, so the key goes straight to Claude.)
  • Pasted images stay on the server. Each workspace keeps only its latest pasted image, in ~/.claude-wrangler/clipboard/<name>/ (readable only by you), which destroy removes. Claude can read that directory without asking (--add-dir), and nothing outside it.

Releases

Crow CI (.crow/) vets and tests every push and pull request, with Go 1.22 (the oldest go.mod allows) and a current Go, and checks that the macOS build works. To release, tag and push:

git tag -a v0.1.0 -m "v0.1.0" && git push origin v0.1.0

Once the tests pass, .crow/release.yaml builds linux binaries for amd64, arm64 and 32-bit ARM (ARMv6 and later, so every Raspberry Pi), and darwin binaries for amd64 and arm64 (scripts/build-release.sh). It then publishes them, with SHA256SUMS and notes listing the commits since the previous tag (scripts/release-notes.sh), as the Forgejo release for the tag, using woodpeckerci/plugin-release. A tag with a -, like v0.2.0-rc1, becomes a pre-release, and re-running the pipeline replaces the release's files.

It needs a Crow secret FORGEJO_TOKEN: a Forgejo access token with only the write:repository scope. Allow it only for the tag event and the woodpeckerci/plugin-release image, so no other build or step can read it.

claude-wrangler version prints the version. Release builds set it with

go build -ldflags "-X main.version=v0.1.0"

go install git.jshaver.net/jshaver/claude-wrangler@v0.1.0 reports v0.1.0 without that, and a build from a clone reports dev-<commit> (plus +dirty with uncommitted changes) or a Go pseudo-version.

Development

go test ./...
go vet ./...

The tests use a fake systemctl, a fake screen -ls, temporary directories and a scripted prompt, so they never touch your real units, sessions, workspaces or ~/.claude.json. The SSH side of the remote commands needs a real host; the commands they send are checked by running them through sh.

File Contents
main.go Argument parsing and dispatch
config.go The config file
env.go The system interface (paths, systemctl, screen, terminal prompts)
workspace.go Units and the local commands
trust.go The ~/.claude.json trust update
connect.go connect, nvim and shell (remote, over SSH)
local.go connect, nvim and shell for a workspace on this machine
completion.go zsh and bash completion
doctor.go, version.go, update.go doctor, version and update
.crow/, scripts/ CI and release builds
clipboard.go Ctrl+V image paste: the laptop side in connect and the server side
skill.go, skills/ The Claude Code skill and install-skill

License

MIT; see LICENSE.md.