Skip to content

Troubleshooting

Start with ws doctor: it checks most of what can go wrong and tells you the command that fixes each problem. This page lists every check ws doctor makes, the doctor badge in the top bar, where to find the logs, and what to do about the problems people run into most: a server from another build, sockets left by a crash, a subcommand that ran as a program, agents that show no status, copying text, and keys that don’t arrive.

The prefix key is Ctrl-Space: prefix I means press Ctrl-Space, then I.

Terminal window
ws doctor

ws doctor prints its checks in groups. Each line starts with a mark:

Mark Meaning
✓ Fine.
! A warning: ws works, but something is missing or could be better.
✗ A problem: something ws needs is broken.

Warnings and problems have a line starting with → underneath, with the fix. The last line sums up: All good., N warning(s); ws works., or N problem(s) to fix. The exit code is 1 when there is at least one problem, so you can use ws doctor in scripts.

ws doctor -L <name> checks the server and saved session for that server name instead of the default.

ws doctor --json prints the same checks as a JSON array, one object per check with area, name, level (ok, warn or fail), detail and, for warnings and problems, fix. The exit code is the same as without --json.

A running server runs ws doctor --json in the background when it starts and every 30 minutes after. While any check fails, the top bar shows ✗ doctor: N. Warnings don’t show a badge. Click the badge, or choose Run ws doctor in the general menu (right-click the top bar), to open a tab with the full report. The server checks again about a minute after you open that tab, so the badge clears soon after you fix the problem. See The interface.

Check Result Fix
git ✗ if missing or older than 2.17 (worktrees need it) Install or upgrade git.
nvim ! if missing; ✗ if older than 0.10 brew install neovim
lazygit ! if missing brew install lazygit
claude ! if missing Install Claude Code.
codex ! if missing (it’s optional) Install Codex.
gh ! if missing brew install gh
gh auth ! if gh auth status fails. Checked only when gh is installed. gh auth login
Check Result Fix
Claude Code ! if ~/.claude/settings.json doesn’t exist, has no ws hooks, or is missing some of them (it lists which). ✗ if the hooks run a ws binary that doesn’t exist. ✗ if the file isn’t valid JSON. ws integrate claude (add --bin /path/to/ws to point the hooks at a binary that exists). Fix invalid JSON by hand; ws won’t edit it.
Codex The same checks for ~/.codex/hooks.json (or $CODEX_HOME/hooks.json). Checked only when codex is installed. ws integrate codex
Codex hooks ✗ if codex features list says hooks is off, so Codex never runs the hooks. ! if ws couldn’t read the list. Checked only when codex is installed. ws integrate codex, which sets features.hooks = true in ~/.codex/config.toml.
Codex autostart ✗ if Codex’s daemon_auto_start feature is on: every Codex session then runs through one shared background daemon, which runs the hooks with the environment of the pane that started it, so status lands on the wrong pane (or on one that’s gone). ! if ws couldn’t read the list. ws integrate codex, which sets features.daemon_auto_start = false.
Codex daemon ! if Codex’s shared daemon is running (its control socket, ~/.codex/app-server-control/app-server-control.sock, answers). Sessions using it report to the wrong pane. codex app-server daemon stop, then start Codex again. Stopping it ends the sessions using it; codex resume picks them up.

The hooks check passes only when every event ws listens to is hooked: 7 events for Claude Code and 8 for Codex. A hook that runs a bare ws counts as existing, since it is found through your PATH when it runs.

Check Result Fix
templates ! if there are no templates in ~/.config/ws/templates/. ✗ if a template file can’t be read or parsed. Copy docs/examples/templates/design-system.toml there and edit it, or fix the file.
template <name> ✗ if the template’s repo isn’t a git checkout. Fix repo in the template.

See Configuration for the template format.

These checks look at the terminal you run ws doctor in. Inside a ws pane they always pass, because ws sets both variables in its panes, so run ws doctor from a plain terminal to check the terminal itself.

Check Result Fix
true color ! if COLORTERM isn’t truecolor or 24bit. Use a true-color terminal, such as Ghostty.
TERM ! if TERM is unset or dumb. Run ws in a real terminal.
Check Result Fix
socket dir ✗ if anyone but you has any permission on the socket directory. Anyone who can reach a ws socket can drive your agents. chmod 700 <socket dir>
server Says whether the server is running, and on which socket. Never a problem.
sockets ! if sockets were left by runs that crashed (sockets nothing answers on). ws doctor --fix
saved session Says whether there is a saved session and, if the server isn’t running, that ws will offer to restore it. ✗ if the state file can’t be read. Move the file aside: mv <file> <file>.bak.
worktrees ! if a workstream in the saved session has a worktree that is gone from disk. Restoring opens those tabs in a fallback directory. Otherwise, a count of the worktrees on disk and how many aren’t open in the saved session. git worktree prune, in the repo, tidies git’s records.
Terminal window
ws doctor --fix

--fix asks first:

Remove stale sockets and create missing ws directories? [y/N]

If you answer y, it:

  • removes every socket in the socket directory that nothing answers on;
  • creates the state directory (~/.local/state/ws) if it’s missing.

It prints what it did, then runs the checks as usual. It changes nothing else. Other fixes, such as ws integrate or chmod, you run yourself. With no terminal to ask on, --fix does nothing.

The server writes its log to one file:

~/.local/state/ws/ws.log

($XDG_STATE_HOME/ws/ws.log when XDG_STATE_HOME is set.) Every server and --local run appends to it. It records server start and stop, restore problems, save errors, hook and socket errors, and processes that needed SIGKILL on quit. When it grows past 10 MB, the next server or --local run moves it to ws.log.1 (replacing the one before) and starts a new one.

Terminal window
tail -f ~/.local/state/ws/ws.log

The server has no terminal to print a panic on. When it panics, it writes the message and a backtrace to ~/.local/state/ws/crash-<time>.log, where <time> is in Unix seconds, and notes the file in ws.log. Include the crash log when you report a bug.

A panic inside one pane’s threads marks just that pane as failed; the other panes keep running.

After a crash, your session was saved as it ran. The next ws offers to restore it. See Sessions.

ws waits up to 10 seconds for a new server to open its socket. If it doesn’t, you see one of these:

the ws server exited during startup (...); see ~/.local/state/ws/ws.log
the ws server didn't open <socket> in time; see ~/.local/state/ws/ws.log

Read the end of ws.log for the reason. A common one is a command that doesn’t exist: ws somecommand starts a server whose first pane runs somecommand, and the server stops if it can’t start it.

Any word after ws that isn’t a subcommand is a program to run. So a typo, or a subcommand quoted as one word, doesn’t give a usage error. Instead, ws starts a server (or attaches to the running one) and tries to run it in a pane:

Terminal window
ws "tab new" # wrong: runs a program named "tab new"
ws tabs list # wrong: "tabs" isn't a subcommand
Terminal window
ws tab new # right: separate words, spelled as in `ws --help`

If this happens, close the pane or tab it opened. If it started a new server that you don’t want, stop it with ws kill-server. The subcommands are listed in Command reference.

“This server is from another ws build”

Section titled ““This server is from another ws build””

After you rebuild or reinstall ws, attaching to a server that is still running the old build shows this in the top bar:

this server is from another ws build: `ws restart` switches, keeping the session

The old server keeps working. To switch to the new build:

Terminal window
ws restart

This stops every pane’s processes and starts a server on the new build that puts the layout back, with agents resuming their conversations. Commands that were running in shells need starting again. See Restart, keeping the session.

“The running ws server is too old for this command”

Section titled ““The running ws server is too old for this command””

A newer ws command talked to an older server that doesn’t know the command, for example a scripting command added in a later build:

the running ws server is too old for this command (...); install the new build, then `ws kill-server` and `ws`

The fix is to restart the server on the new build, as above. If the old server doesn’t know ws restart either (it says it’s too old), use ws kill-server and then ws. That doesn’t keep the session, so let your agents finish first. Some scripting commands say it may be too old when the server’s reply is missing something they need; the fix is the same.

When a server or a --local run crashes, its socket file stays behind. Nothing answers on it. This is mostly harmless:

  • A new server removes a stale socket with its own name before it starts.
  • ws doctor warns about the rest, and ws doctor --fix removes them.

If a command says no ws server is running on <socket> or no ws server answers on <socket>, the server for that name isn’t running. Start it with ws, or check that you are using the right -L name.

If starting a server fails with <socket> is already in use, a server with that name is running already. Attach to it with ws attach.

An agent that ws found by its process name but that has never sent a hook event shows no status, with what to do next (no status: ws integrate claude, or for Codex no status: see ws doctor). Work through these in order:

  1. Are the hooks installed? Run ws doctor and look at “Agent hooks”. Install them if needed:

    Terminal window
    ws integrate claude
  2. Was the agent running before you installed the hooks? Agents read their hooks when they start. Quit the agent and start it again.

  3. For Codex, are its features right, and is its daemon stopped? ws doctor checks “Codex hooks”, “Codex autostart” and “Codex daemon”. Codex’s shared daemon runs every session’s hooks as if they came from the pane that started it, so they report the wrong pane. ws integrate codex sets the features; stop a running daemon with codex app-server daemon stop and start Codex again.

  4. Do the hooks run a ws that exists? If you installed hooks from a build that has since moved or been deleted, ws doctor shows ✗ with “hooks run …, which doesn’t exist”. Run ws integrate again from the installed ws, or pass --bin.

  5. Is the agent running in a ws pane? Hooks report to the server through WS_SOCKET and WS_PANE_ID, which only ws panes have. Outside ws, the hooks do nothing, on purpose.

The inspector shows why an agent has the status it has. Open it with prefix I for the focused pane, i on an agent in the sidebar, or “Inspect agent status” in the pane’s or tab’s right-click menu. It shows:

  • the status, how long it has held, and what set it: a hook event, a process detection, or you;
  • the agent’s identity: kind, model, session id, whether hooks are reporting, and where its label came from;
  • the pane’s foreground process and its children;
  • a timeline of the latest hook events, status changes and detections.

In the inspector, c copies a report of everything shown to the clipboard and saves it as ~/.local/state/ws/status-report-<pane>-<time>.txt, and d, w, b or x set the status by hand until the agent’s next real event. Agents covers the rest.

The same history is kept in the state file, so you can read it from any shell, even when the server isn’t running:

Terminal window
ws debug events --since 10m
Terminal window
ws debug events --since 2h --pane 3
Terminal window
ws debug status 3

ws debug events lists hook events, status changes and detections, oldest first. --since takes a number with s, m, h or d (default 10m), and --pane limits it to one pane; the pane number is in the inspector’s title. ws debug status <pane> shows a pane’s saved agent identity and its last 20 events. Times are shown in UTC. Add -L <name> to read another server’s history.

If there are no hook events at all for the pane, the hooks aren’t reaching ws: go back to the checklist above. If events arrive but the status is wrong, the timeline shows which event set it; include the inspector report (c) when you report it.

“Could not resolve to a Repository” when reviewing a PR

Section titled ““Could not resolve to a Repository” when reviewing a PR”

GitHub says this when the gh account in use can’t see the repo, typically a private org repo while your active account is a personal one. ws review tries your other gh logins itself, and the review tab uses the one that works. If it still fails:

  • Run gh auth status and check that an account with access is logged in; if not, gh auth login.
  • Or name the account in ~/.config/ws/config.toml: [review.repos.<alias>] gh_user = "<account>".

See Reviewing PRs.

Drag across text in a pane to select it. When you let go, ws copies it to your clipboard. Double-click selects a word (paths and URLs included), and triple-click selects a line. To select with the keyboard, or text that has scrolled off the screen, use copy mode (prefix [); see Keys and mouse.

ws copies through your terminal, with the OSC 52 escape sequence, so it works over SSH too. Your terminal has to support OSC 52 and allow it:

  • Ghostty, iTerm2, kitty and WezTerm support it. Some terminals ask before letting programs write the clipboard, or have a setting for it; in iTerm2, look for the setting that lets terminal apps access the clipboard.
  • Terminal.app doesn’t support OSC 52.
  • If you run ws inside tmux, tmux has to pass clipboard writes on (its set-clipboard option).

If nothing lands on your clipboard, try another terminal first.

Selecting in nvim, lazygit and other mouse programs

Section titled “Selecting in nvim, lazygit and other mouse programs”

Programs that use the mouse themselves, such as nvim and lazygit, get your clicks and drags as usual. Hold Shift while you drag to select text in ws instead. Some terminals use Shift-drag for their own selection; if yours does, you get the terminal’s selection, which doesn’t know about pane borders.

Typing or clicking clears the selection.

  • On macOS, Ctrl-Space is also the default shortcut for switching input sources. If it’s taken, turn that shortcut off in System Settings, under Keyboard, Keyboard Shortcuts, Input Sources.
  • To send a literal Ctrl-Space to the program in a pane, press it twice: prefix Ctrl-Space.

Keys such as Alt-h and Alt-1 need your terminal to send Option as Alt. In Ghostty, set:

macos-option-as-alt = true

Other terminals have a similar setting.

Alt-[ doesn’t work, or Shift-Enter doesn’t reach the agent

Section titled “Alt-[ doesn’t work, or Shift-Enter doesn’t reach the agent”

Without the kitty keyboard protocol, some keys send the same bytes as others:

  • Alt-[ sends the same bytes as the start of an escape sequence.
  • Shift-Enter and similar keys look like their plain versions.

When your terminal supports the kitty keyboard protocol, ws turns it on, and these keys come through distinctly. Ghostty, kitty and WezTerm support it; tmux and Terminal.app don’t. In other terminals, use Alt-] or prefix 1 to 9 to change tabs.

Collect these before you ask for help or file a bug:

  • the output of ws doctor;
  • the end of ~/.local/state/ws/ws.log, and any crash-*.log files;
  • for a status problem, the inspector report (c in the inspector) or the output of ws debug events --pane <n>.

See also Sessions, Agents and Configuration.