The cases-folder refusal grew to 143 characters, and the status line is one
line with an ellipsis and no title, so at 513px on desktop it ended at "or
make the case with Ad..." and lost the instruction. .path-picker-status.error
now wraps: 2 lines on desktop, 4 on a phone, measured clear in Chromium at
both, and it covers the picker's other refusals too.
Tests: a pin on that rule (fails without the fix), and the option check now
reads each picker's whole options object instead of a fixed 400 characters,
which stopped short of the container-workdir picker's 829. Retitled to say
what it checks: Create New has the flag too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the folder field empty, Create New's Browse opens in the cases folder, and New
folder there answered "Use Add Case > New Case". There is no tab of that name (the
label is Create New), and a Create New user is already on it. The refusal now reads
right from Link Existing and from Create New's custom folder. Its test pin, the code
comment, the manual, api-reference, CLAUDE.md and architecture-invariants move with it.
The block() helper in the New folder picker test returned an empty string for a name
that is not in session-ui.js, so .not.toContain passed and a renamed picker dropped out
of the check. It now asserts the name was found.
The manual's path picker list said two places and left out Create New, which the New
folder paragraph names. It now lists every Browse that opens the picker, and that
paragraph is rewrapped to the page's width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tcsh login shell worked on 1.48.0 only because the local sh rewrote
${SHELL:-/bin/sh} to this host's shell. With the launch verbatim, tcsh gets -i -l,
refuses it and every stock launch leaves a dead pane. The changeset and
docs/remote-sessions.md described that as a standing limitation; they now name it
as a change in this release, with what the override workaround costs.
Also: extraSshOptions joins the list of values now literal at launch, the stock pane
command is shown as /bin/sh -c in the docs and the builder's JSDoc, the invariants
state how the launch must reach the far end, the manual pages cover $ in an override,
and the invariants no longer say extraSshOptions refuses a bare $ (it never did).
Reworded the em-dashes on the invariants line this rewrites.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ownerClampedEnvKeys() read enabledClis(), so disabling a CLI dropped its
privileged keys from the clamp, and a revoked owner got a saved home back on
recovery or reboot restore. getCli() still returns a disabled entry, and a
session can outlive the switch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getCustomModelForPersist() trusted homeEnvVar to name a path, but the registry
types it as any env name, so a clis.json override could write a session's own
credential to state.json. Only an absolute path is written now.
A re-apply resolved the CLI's real home from what the session's map held, which
under a selection is the injected directory, and recovery resolved none at all.
An entry the own home lacks (settings.yaml) was then linked from the server's
home on an endpoint switch and on recovery. The route now reads the new
Session.ownEnvValue(), and recovery reads the clamped record through the same
bounded guard, under the bulk cap since it walks every saved session. Also
corrects the comment that said only a removed directory gets its links again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RemoteCommandOverridesSchema is .strict() and stopped at deepseek, so the API
refused a commands.omp or commands.copilot override although both modes can run
remotely and in a container (RemoteCommandMode, DockerCommandMode) and the launch
builders honour the key. The docs call the override the way out for a login shell
the stock launch cannot run, which was not true for those two modes.
The test walks the CLI registry, so the next CLI cannot miss the schema the same way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rename's checks inside mutateLinkedCases and the refused-registry
branches of rename and clone had no test: removing them left every
case route test green. A hook right after the folder move lands a link,
an unlink, a re-pointed entry or a torn file between the route's first
check and its registry write. Each new test fails when its check is
removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lookup judges liveWorktree.path lexically and only when the pane moves,
while the route's probe and readdir both follow symlinks. A symlink put at
that path, or at a parent of it, without moving the pane re-rooted the listing
outside workingDir. Names and sizes leaked, while file-content and file-raw
still refused the paths.
The route now resolves both folders on every request and walks the worktree
only when it lies strictly inside the resolved workingDir, otherwise it walks
workingDir. Only the check uses the resolved paths: the walk keeps the session's
spelling, so a workspace reached through a symlink still gets paths relative
to workingDir.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
transcriptPageKeys applied to every pi, and a pi in `tuiMode: regular` binds
PageUp/PageDown to its editor. It emits no mouse enable, so the flag stays
false, and on a fresh pane the buffer is hollow: the wheel sent PageUp, the
caret of a multi-line draft moved, and the next keystroke landed on the wrong
line. Measured on real pi 1.1.0, 1.4.0 and 1.5.0. Regular mode is not exotic,
it only takes one line in pi's settings. Telling the two modes apart would
need a new capability, since the paging gate takes no id, so pi has no
fallback: a fullscreen pi with "Wheel scrolls local history" on stays where
master left it.
altScreen, the half of the fix that does the work, was pinned by nothing:
reverting it left 1328 tests green. The strip decision table reads its
expected value from the same registry entry. pi now has its own block using
the bytes it really emits, which also pins that focus reporting survives.
The fullscreen default dates from pi 1.0.0, not 1.4.0, so the comments say
that, and the sites still calling pi narrow-strip or main-screen by default
are corrected. Where a comment listed CLIs it now names the capability.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Handing the launch to bash verbatim exposed `"${SHELL:-/bin/sh}"` to the far
end for the first time. tmux runs a one-argument command through the remote
default-shell, not /bin/sh as the comments said, and fish refuses `${...}`:
on a fish host, or a bash login with `set -g default-shell fish`, every new
pane died with status 127. master only survived because the local sh had
already rewritten that token to this host's shell path.
Codeman's own text now goes to `/bin/sh -c` as separate tmux arguments, which
tmux execs without a shell. A commands.* override stays one argument and keeps
the default-shell it was written for: under `/bin/sh` a `source` or `[[` in an
override dies with 127 on bash and zsh hosts.
The tests read the argv tmux would receive through a real sh, so they need no
fish: every stock launch must reach /bin/sh and an override must not. The
docker half of bashCArg was pinned by nothing, so it gets its own test.
Comments that described the removed double-quote layer are corrected, and
remote-hosts.ts no longer claims overrides are seeded from the default text
or that they rescue nushell and elvish.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude Code's EnterWorktree moves the running process into
<repo>/.claude/worktrees/<name>, while Session.workingDir stays on the main
checkout. The Files panel kept walking workingDir, so it listed a tree the
agent had left.
paneCwd already follows the move, measured on Claude Code 2.1.318 in both
directions, so nothing new has to track the agent. It also follows every cd
in a shell, so a new SessionState.liveWorktree narrows it to a linked
worktree, read from the .git file without starting git.
Only a folder strictly below workingDir is ever an answer, and the listing
only starts there: returned paths stay relative to workingDir, so the
preview, raw and edit routes are unchanged and confinement does not widen.
A worktree outside the session folder stays unfollowed.
The panel asks with root=live, shows a strip naming the worktree with a
switch to the session folder, and follows the agent in and out while open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pi 1.4.0 is an alt-screen TUI that enables mouse tracking once at start.
Codeman's replay (tab switch, reload) drops that mode in xterm
(mouseTrackingMode none, baseY 0), so the wheel hit an empty local buffer
and stayed dead until a resize made pi redraw.
Same shape as copilot: altScreen strip-mux-and-mouse (the server records
the stripped DECSETs as cliMouseTracking, which survives the replay),
wheelForward always, transcriptPageKeys (PageUp/PageDown fallback for a
stale flag, measured on pi 1.4.0). _shouldForwardWheelToApp treats pi like
copilot. Registry pins and docs updated.
Tests: 2 new in terminal-scroll-routing (red before). Full run: 12989
passed, 5 red from the de_DE locale / XDG env (custom-model-run-menu-ui,
git-worktrees, edit-history-sync), all green under LANG=C.UTF-8.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 53e4136d9f4401e9250ac6a122599ffcb2a57e8a)
respawn-pane runs `bash -c "<ssh …>"` through the local sh, so every `$` in
the remote command is expanded on the Codeman host. withRemoteTmuxPath
(`export PATH="$HOME/.local/bin:…:$PATH"; tmux …`, 5b9445de) therefore sends
the Codeman host's home and PATH to the remote host: a remote user with a
different home (tmux in ~/.nix-profile/bin) gets "tmux: command not found" and
every new remote pane exits 127. `${SHELL:-/bin/sh}` in the remote launch was
the local shell for the same reason.
Remote and docker commands are already quoted for the far end, so they now go
to `bash -c` single-quoted (bashCArg); local commands keep the double-quoted
form. Two tests run the captured respawn-pane line (create and respawn)
through a real sh with a foreign HOME/PATH and check that ssh gets `$HOME` and
`$PATH` unexpanded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2818e9455489b9acd65841258df4cd035609954c)
Applying a custom model writes the endpoint's values over the keys it
injects, and clearing the model unset those keys. A session that had set
one of them itself lost that value: its own DSH_HOME, CODEX_HOME,
GROK_HOME or GEMINI_API_KEY was gone once the model was cleared, and so
was CLAUDE_CONFIG_DIR, which silently moved a Claude session onto the
default account. Switching endpoints did the same.
setCustomModel() now records what the session held under each injected
key and restores it on clear and on switch. Quick-start passes the
caller's values to the Session constructor so they are seen before the
selection lands.
The selection overwrites the only other copy, tmux's setenv, so the
session's own home directory has to survive a Codeman restart on its
own. __customModel therefore stores that one path, the single exception
to persisting keys only, and recovery runs it through
clampEnvOverridesForOwner() so a revoked grant cannot bring it back. Any
other own value, a credential included, stays in memory only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five routes changed the linked-cases registry: link, unlink, rename, and a
create or clone into a custom folder. Each read the file, checked it and wrote
it back on its own, so two requests that both read before either wrote lost
an entry. Two clones that claimed one name and finished together both passed
the name check, both answered success, and the later write replaced the
earlier entry. The same race made "a name linked while the clone ran is a
409" in test/routes/case-clone-custom-folder.test.ts fail about half the time.
- New src/web/linked-cases-writer.ts, mutateLinkedCases(), shaped like
mutateSettingsFile(): mutations run one at a time, each write is a temp
file renamed over the registry (through a symlink, keeping its mode), and
a file that is not a JSON object is refused (409 CONFLICT) instead of being
read as {} and overwritten.
- All five writers go through it, with their name checks inside the
callback, so exactly one of two requests for one name wins. Rename also
re-checks that the old name still points at the old folder, and drops its
manual registry undo, since a failed write no longer changes the file.
- Tests: test/linked-cases-writer.test.ts (writer behaviour, and concurrent
link/unlink through the routes with slowed writes, which fail on the old
code). Tests that forced a failed registry write with chmod 444 or a
directory now inject the failure at the rename, or expect the refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gate belonged to whichever read arrived first, and sessions load their stores
independently, so under CI timing another session took it, the loop never reached
release() and the test timed out (master, CI run 41930703100). The code cannot block
one session on another: sync() only waits on its own slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consolidates the four contributor changesets, and rewords the em-dashes this release
added in comments and docs. The five in test/settings-save-failure-ui.test.ts stay:
they assert existing toast text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clone ran an unbounded realpath over every Docker case workspace, and the mkdir
route inherited the picker resolver's realpathSync, which freezes the whole server
on a mount that stopped answering. The picker's browse route has the same call and
is left as it was.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dedicated route got this order at merge and quick-start kept the old one, so
a user could agree to a swap only to get the 400. It also runs on the pure build
now, so a refused id writes no config dir to clean up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A table costs its widest cell times its rows, so one wide cell in an agent's last
message built a 202 MB string on the event loop although only 8 posts can ever go
out. Only what can be posted is converted now. A file a person sent from Discord
also fired attachment:detected as if the agent had produced it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Discord bot token was readable through the attachment routes, because
discord.json was not on the sensitive list, and so were webhook.json,
custom-model-hosts.json and the generated custom-model configs. Config backups
were the wider gap: a dated copy of users.json or settings.json matched none of
the name-anchored patterns. All are blocked now.
`codeman notify --file` relied on folder confinement alone, which holds nothing
back when a tab's folder is the home directory, and it would also post a
workspace .env. It now applies the attachment blocklist to the real path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision. Typing the note into a brand-new non-Claude tab spent a model
turn per tab and, for a spawned worker or a scheduled run, landed with the first
real prompt. Claude keeps its hint through the system prompt, and every other CLI
is told nothing. The setting goes with the path it gated: sessionIdle, the fresh
set, isHintableSession and the session:idle wiring. sendInput loses `fromUser`,
which was only ever false for the hint, and the deps test pins that a reply is a
person's own input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`cwd` beside the fixed `workingDir` reads ambiguously on a public API surface, and
`paneCwd` says where the value comes from, like `paneExit`. Also pin the field on
the payload the browser really receives (getSessionStateWithRespawn), and give the
pane-directory text its own architecture-invariants paragraph.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Bash( row holds a command, so its parentheses do not bound one spaced path:
`Bash(/tmp/a.out input.txt)` became a single link to a file that cannot exist and
lost master's `input.txt` link. The path tools keep linking a spaced name whole,
and a hard-wrapped Bash( row gets no spaced lookup either. Test rows from #734.
Co-Authored-By: DDT <141296842+Hugo-DDT@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Say that settings.yaml is shared only until the session's first settings write
(dsh saves by rename, which replaces the link), warn when a session's own DSH_HOME
does not answer and the server's home is linked instead, test
guardSessionConfigDirHome, and document the linkFromRealHome exception and what a
custom-model dsh session does not carry over.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The launch-id check is pure, so it now runs before the context warning and the
swap confirmation: nobody agrees to evict a model only to get the 400. Also the
search-box and in-use comments, the model-plain-colon wording, and the wiki's
order of the picker's marks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new install has no seen list, so every new OpenCode user was still warned about
an October incident that is long over. The mechanism stays for the next notice;
an empty list is now a valid file, and the validators keep running over whatever
it holds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Classify OpenSSL and LibreSSL's SSL_connect errors as unreachable again (`_` is a
word character, so \bssl\b missed them and the user read the temp directory as the
reason), point a cases-folder refusal at Clone Repo instead of Create New, and move
the mechanism and history out of CLAUDE.md into architecture-invariants.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
selectSession() toggles .active at once while _syncTabStripCorner() only runs in
the debounced render, so a cap set after measuring left a newly selected first
tab wider than the room and dropped the first row below the toolbar until then.
max-width changes nothing on a narrower tab, so the cap is simply always on, and
the clear-before-measure and finish-the-animation loops go. The no-usable-room
test uses 8 tabs: at 40 the scroll fallback fires first and it passed at any
threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>