Clone Repo's picker now offers New folder, and with the field empty Browse
opens in the cases folder, so the first press there lands on this refusal.
It pointed at Add Case > Create New, which cannot clone. The sentence has to
read right from every picker that offers the button, so it names both tabs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clone Repo's custom folder became a parent that must already exist (#774),
while its picker still had no New folder button (#761 turned it on for
Create New only). A parent that does not exist yet was refused with no way
to make it from the form, so the same job worked on one tab and dead-ended
on the other.
The test pin that kept Clone off dated from when the field was the exact
target, which could be a folder not yet created. Clone moves to the on side
of that pin, so every picker in the file stays covered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Edit history panel told an adopted session it needed to be a Claude
session outside Docker, and a stop wait blamed the missing Claude Code hooks
on the mode. Both refusals are right, only the words misled. Each now says
Codeman did not start the session, with a zh-CN entry for the panel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a foreign session ended, remain-on-exit kept Codeman's own tmux client
on the wrapper's dead pane, so the tab kept its pid and busy, input went
nowhere, both attach routes refused it, and a new session of that name only
led back to the dead tab. paneExit stays out of it by design.
The reconnect watcher's tick now detaches a client from a dead wrapper. That
is an ordinary PTY exit with status 0, so the pid goes null and selecting the
tab runs the attach again: it joins the target if it is back and otherwise
prints the existing guard text. Checked on an isolated instance and in a
headless browser: boot recovery, target gone, target back, view detached, tab
closed, and no restart loop while the tab sits unselected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The remote file reads ran under the user's login shell, quoted for a POSIX
one. Under fish the probe was a parse error, so previews failed, and csh and
tcsh failed every multi-line read. Wrapping the probe in sh -c as the review
proposed would have opened a hole: fish reads \' inside single quotes as a
quote, so nested shell quoting flips, and a path holding ';touch x;' ran
touch x (measured, also on the existing folder listing). csh and tcsh run a
multi-line command line by line, so a newline in a path did the same.
buildRemoteFileCommand now sends every script as one word of printf text
with those bytes in octal, and only sh reads it. Probe, folder listing, read,
range read and transcript lookup gave the same result under bash, zsh, dash,
fish, tcsh and csh in a throwaway container ($SHELL -c, no real sshd), and 24
hostile paths ran nothing. Plain csh caps a word near 8 KB, noted in the docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parser and classifier run synchronously on the event loop over text a
pane, a container or an ssh host wrote, and one process could stall the
server for every session: 4 s for a 128 KiB argv of `/pi/` tokens, about a
minute for 2 MiB of socket rows.
The `cli.js` launcher rule leaves the signature regex for a per-word scan,
socket rows dedupe through a Set, the process row regex takes the `s` flag
(a bare CR made `$` fail and `\s+` give its spaces back, quadratic in a row),
argv and pane command are cut at FOREIGN_MAX_ARGV_CHARS, and one matcher per
scan answers each distinct argv once, since every pane of a hostile host can
claim the same process tree.
Measured on a private tmux server with the same 131 KiB argv: the largest
event-loop gap went from 3991 ms to 45 ms. The worst scan the caps allow
takes about 80 ms. Socket rows stay uncapped on purpose (see the docs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`POST /api/sessions/:id/run` had no adopted gate, so while the wrapper had no
attached PTY it spawned `claude -p --dangerously-skip-permissions <prompt>`
in /tmp, a launch every other adopted route refuses.
The route answers INVALID_INPUT, and `runPrompt()` refuses too, which also
covers the legacy `sendInput()` path and leaves no direct spawn in Session
that an adopted session can reach.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wrapper that could not start fell back to a direct PTY: `claude
--dangerously-skip-permissions` or a server-account shell in /tmp on this
host, under a tab still carrying the foreign session's name, so someone who
opened an ssh or container session ended up typing into this machine.
startInteractive and startShell now fail the start for an adopted session
and leave `_useMux` on, so selecting the tab tries the wrapper again. The
`error` event carries the reason, since the browser's re-attach does not
read the route's answer. A session with no mux at all is refused the same
way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A claude session on its own CLAUDE_CONFIG_DIR launched on an endpoint with
<isolated>/projects linked to the server's tree. The resume pin looks there, so
apply found no transcript and started a new conversation, and on clear the
endpoint's transcript stayed behind in the server's tree.
The link now goes to <own dir>/projects when the session set one, so the
conversation and what the endpoint run adds stay on the session's own account
across an apply, a clear and a restart. It goes through linkRealHomeEntry(),
which also creates a missing projects folder instead of leaving a dangling
link, in the own dir and in ~/.claude/projects on the default path. A relative
CLAUDE_CONFIG_DIR of the server gets no link, since joining it would create
projects under the server's working directory.
Rule #516: guardSessionConfigDirHome() now covers configDirVar, so all three
apply sites (route, quick-start, boot recovery) probe the own dir bounded before
the apply touches it, and a dir that does not answer falls back to the server's
tree. Under test, HOME is a temp dir and CLAUDE_CONFIG_DIR is stripped by
test/setup.ts, which a new test pins.
Measured on a real Codeman server with a fake claude: apply and re-apply resume
under the isolated dir with the transcript landing in the own dir, a clear
resumes on the own dir, the isolated dir goes while the transcript stays, and the
same holds across a server restart. The documented limits come out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A remote host whose login shell is tcsh came up on 1.48.0, when the local
sh rewrote ${SHELL:-/bin/sh} to the Codeman host's shell. Since #770 the
launch reaches the far end verbatim and runs tcsh itself, and tcsh refuses
-i -l -c, -l -c and -i -l, so every stock launch left a dead pane.
The login flags are now picked on the far end, in the shape of
loginShellArgs(): set -- -i -l, cleared for csh and tcsh, then
exec "${SHELL:-/bin/sh}" "$@". The text is one brace group, so
cd <path> && { ...; } still starts nothing when the cd fails. bsd-csh takes
every flag combination, csh is listed because it is tcsh on macOS and the BSDs.
The version probe keeps its one statement: the login shell parses it, not
/bin/sh, so fish and tcsh refuse it either way, as they did on 1.48.0.
Measured with the real builder on a real sshd in a throwaway Debian container
(tcsh 6.24.07, bsd-csh, fish 3.6, bash) and through a fake sshd with
tcsh 6.24.10, fish, zsh, dash and bash, a real Codeman server on tcsh included.
The tcsh disclosure comes out of the docs and the changeset. They keep the two
limits 1.48.0 had too: tcsh has no export, so the PATH bootstrap only prints an
error, and the tmux probe needs a command executable, which Debian lacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The attach line now takes the rule #770 gave remote and docker launches: bashCArg() single-quotes it at both respawn-pane sites, so it reaches bash verbatim. quoteAdoptLaunch is retired, since its double quotes kept the same $ and backtick safety but lost a real newline. #510's no-cd condition stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Will clone into" line named a folder the request would not use: the
preflight replaces the page's guessed case name with the server's (for a
URL such as .../repo?tab=readme the guess is repo-tab-readme and the server
says repo) and nothing refreshed the preview, so it stayed on the old name
while the field and the request moved on. It is now refreshed there too.
The Browse picker's title and the empty-field toast still said "Choose the
folder to clone into", the old exact-target meaning. Both now ask for the
parent folder, matching the field's label.
CLAUDE.md and the invariants paragraph split the two meanings: path on the
API is the exact folder, the Clone Repo tab's field is a parent and the page
sends <folder>/<case name>.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_renderMarkdown` wrote dir="auto" into its string, before the code pins and
the linkifier. An element carrying dir="auto" re-resolves its direction on
every insertion under it, and a block whose letters all sit in `dir` children
walks its whole subtree each time, so a code-only or path-only document went
quadratic, English ones included.
Measured in real Chromium through the real File Viewer, before and after:
9000 inline-code items 8.7 s to 0.35 s, 9000 bare paths 5.6 s to 0.23 s, a
209 KB out-of-workspace list 41.9 s to 1.4 s. All three now match master.
The pass is now `_applyBlockDirection()`, run last on the DOM by both
consumers. The direction results are unchanged. Tests pin that the rendered
string holds no dir="auto" and that the pass runs after the linkifier.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
startShell() writes `clear\n` 100 ms after a new mux attach, and for an adopted
session that write reaches someone else's live pane through the wrapper's tmux
client. Measured on an isolated instance: a shell with `echo DRAFT_NOT_SUBMITTED_`
typed at its prompt ran `echo DRAFT_NOT_SUBMITTED_clear` the moment the tab
opened. `shell` is the feature's primary case and also the classifier's answer
for anything it cannot name (vim, psql, a `read` prompt), so every adoption put
those six bytes in front of whatever was reading stdin.
After the fix a byte recorder in the foreign pane saw 0 bytes in the 6 s after
adopting, and a real input afterwards arrived intact. Not in the review: its
recorder covered the trust-dialog path, which is `claude` mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An account sudoers lists by name, with a password, is routed to no-admin
because nothing can read sudoers without reporting to the administrator.
The route itself stays as chosen, but the reason no longer claims the
account may not use sudo, and every message that ends or sums up the route
names CODEMAN_NO_ADMIN=0 for that reason alone.
tmux and cloudflared are staged in the home folder, so a noexec /tmp no
longer reports a working build as one that does not run. USER gets a default
at the top of the script, which also covers the older lines this route can
reach. A trailing slash on CODEMAN_INSTALL_DIR no longer puts .new and .old
inside the install. The data directory prompt names a runtime kept one
prompt earlier.
The uninstall check reads the source and must never drive uninstall(): HOME
does not redirect systemctl --user, so a drive reaches the real service.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Names tmux reads as target syntax (`@1`, `%1`, `=eq`, a bare `~`) passed the
has-session guard by name and then joined something else in new-session -t:
another session, or a fresh shell on the foreign server. They are refused at
discovery now, and take the existing skipped note.
One stopped tmux server emptied the whole local list and left a stuck client
per scan. Each server now gets a bounded ping with stdout at /dev/null first,
because a tmux client hands dups of its stdout to the server: a stopped server
holds our pipe open after the client is dead, so a watchdog around list-panes
cannot work. The probe runs in its own process group, all three transports
keep the rows read before a failure, and a missed ping becomes a note.
The attach and the probe pass -u, so a POSIX-locale container no longer garbles
the tab or lists a non-ASCII name as underscores. A docker attach gets the same
TERM and COLORTERM an owned container's exec gets.
A missing tmux no longer reads as "session is gone", and the launch line's `$`
now reaches bash unexpanded, so the ssh attach's PATH line is expanded at the
far end, as the probe's already was.
fix-plan/write, fix-plan/read and ralph-prompt/write refuse an adopted session:
its workingDir is the neutral /tmp, so they read and wrote a predictable name
in a folder anyone can write to.
FOREIGN_MAX_SOCKETS is gone: it capped nothing, and a real cap in glob order
would never reach `default` on a box with many `codeman-*` sockets (this one
has 212). Closing an adopted tab re-reads the outside-sessions list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The summary and README said rerunning the script updates Codeman. It never
fetches, so that was a successful no-op. The summary, README, its zh-CN mirror
and the wiki now say git pull, then rerun, with the in-app route first.
Also corrects "printed once" (twice at a terminal, never off one), says a typed
answer beats a preset, adds the first-run setup, the 180 s wait, --no-wait and
the changeme refusal to Running-As-A-Service and Troubleshooting, and records in
architecture-invariants why docker/.env is generated FROM .env.example.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With stdout captured (CI, provisioning, `| tee`) the log held the password twice
for a service that controls Docker on the host, while docker/.env, the stronger
copy, is 0600. Off a terminal the script now points at the file instead. The note
beside it no longer promises a second showing, which `--setup-only` never gave.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The refusals of $HOME, ~/.codeman and the checkout compare strings, so
`~/../me` or `<repo>/../repo/data` passed them: the first shares state with a
native install, the second lands in the build context, where `COPY . .` would
bake CLI logins into the image. Doubled slashes are collapsed and any `.` or
`..` segment is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first-run gate tested `! -f`, so a dangling symlink was silently replaced,
and a directory got a 0600 temp file holding a working password, a "Wrote"
message and exit 0, with setup repeating on every later run. Setup now runs only
when nothing is there. A symlink to a real file stays an existing install.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a single-quoted dotenv value a backslash escapes the closing quote, so a
password or data path ending in one passed validation and produced a docker/.env
Compose cannot parse. The error named an unrelated line, printed the password,
and a rerun never asked again. The env_quote comment claimed a single quote was
the only character such a value cannot hold, and the test stub's comment said
bash reads these values the way Compose does: both corrected, and a test now
reads generated values back by Compose's rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compose prefers the shell environment over the env file, so with
CODEMAN_APPDATA_PATH or CODEMAN_PORT preset, a different typed answer went into
docker/.env and the summary while the stack ran on the preset: state split
across two folders and the next plain start mounted another home. Setup now
unsets everything it wrote, so the file it just printed is what Compose reads.
The test stub honours that precedence, and the questions get their first test
through a real pty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`docker info --format` exits 0 with the daemon unreachable on those clients,
so a user outside the docker group got a raw Compose error and a "network
hiccup" hint after docker/.env and the data folder had already been written.
`docker version --format` exits 1 on every release and keeps the same stderr
text, so the permission branch still matches. The test stub now answers `info`
the way those clients do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The round-robin walk capped what it listed, not what it read: every queued
folder was read whole, sorted and held until drained, so one GET could pull
millions of entries (100 folders of 50,000 files killed a 400 MB heap). A read
now keeps only the entries the listing can use, and MAX_WALK_ENTRIES_READ stops
opening folders at 250,000 entries, leaving the rest queued and flagged.
A folder also spent its whole share on its own files, leaving nothing for the
subfolders it had just found, alone in its round or beside a sibling, so search
hits inside them were lost. Its files now count as one more share beside those
subfolders. Math.max(directoriesInRound, 2) would have fixed only the sole-folder
case and cut a wide root's folder list short.
Folders below the walk root are probed before their readdir (#516), since the
walk reaches folders the depth-first one never did.
Filtering at the read also clears the false flag on a folder whose only unread
entries were hidden or excluded, and an exactly-full workspace now finishes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
list-panes -t '=name' with no colon is a window target to tmux, so a
remote session that is gone resolved to a window of that name or to a
session the name is a prefix of, and the Edit history panel moved to that
session's conversation and stored its diffs. `=name:` is an exact session
match, as src/remote-pane-history.ts already uses. Measured on tmux 3.4.
The fake tmux in the test matched the name as a substring, so it passed
with or without the colon. It now needs the exact token and answers a bare
prefix the way real tmux does.
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>
foreign-sessions.js joins the files the no-id test scans, so the catalog lookup
stays one. The block's rules go where a frontend reader looks, in
src/web/CLAUDE.md, and the architecture note no longer calls the file surfaces
read-only, since File Viewer edit mode writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two routes are public under /api/v1, so they get an API reference section.
The overlay gets an architecture note and a CLAUDE.md rule: an adopted session
has session.remote and session.docker undefined and a neutral workingDir, so any
new code that reads workingDir as a workspace must also test isAdopted. That is
the class of bug this round fixed a dozen times.
Records a measured limit: while the view exists an owner's kill-session leaves
the window alive under it until the tab closes.
Comments that still called workingDir the foreign pane's cwd now say what is
true since the wrapper got its neutral folder. Also drops a zh-CN key that
duplicated an existing one, and updates the two structure tests pinned on the
lines this round changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The setting told users the sandbox and approvals stay on. With it on, a
command in Codex's sandbox can call Codeman's API, which can start a session
outside the sandbox with no approval prompt. Measured on codex 0.162.0 with
`codex sandbox`: a touch the sandbox refused landed through POST /api/sessions,
and from inside a Codeman pane it landed with a password set too, because a
pane inherits CODEMAN_PASSWORD. Without that password the same call gets 401.
The settings row, the wiki, the skill and the changeset now say so, the wiki
Security page and security-architecture name it, and the docs scope the claims
to what the code does: app-wide, local launches only, not cron jobs, and
--no-daemon is a local launch's flag. Agent-CLIs gains the --no-daemon
trade-off. A test pins that no surface sells the sandbox as still in charge.
Text only: the switch, the clamp and the launch are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test waited for a real 1000 ms timeout and read the pid file afterwards.
On a loaded machine the kill could land before the fake ssh wrote it. It now
waits for the pid and sends the timeout's SIGTERM itself, which is all Node's
timeout does. Without exec the fake still survives and the test goes red.
The invariants said "the reads" go through buildRemoteEditHistoryCommand; the
image probes go through remoteProbePaths, on the same limiter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every string the block shows now has a zh-CN entry, harvested by a test from the
real module. The row's mode comes from the CLI catalog, not a list of ids that
had already missed omp and copilot. The close dialog's line is static, so the
translator can reach it.
A non-admin's poll answers 403, which read as a failed poll: "Could not reach
the server" under a heading they cannot use, and a request every eight seconds.
That now stops the polling and shows no block.
Wide-scan notes are kept apart from local ones, so the next local poll no longer
wipes "remote X: unreachable" while its rows stay on the list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>