[Feature]: support agents running inside sandboxes

#193 · closed · 5 comments

View on GitHub ↗

eleith

### Preflight - [x] I searched [existing issues](https://github.com/epilande/ccmux/issues) and this has not already been requested. ### Problem / motivation agents like agy / claude / pi can be run in sandboxes (fence / nono / bwrap). however, when ccmux sees PIDs of these processes, it prunes them and doesn't register them as agents. my guess is it looks at the parent pid and short circuits since it assumes they aren't agents. herdr tries to work around this by (a) crawling pid parents to their children (b) allowing users to set environment variables to enable detection: https://herdr.dev/docs/agents/#vms-and-sandbox-wrappers ### Proposed solution let users register their agents manually `ccmux agent register ...` ### Alternatives considered _No response_ ### Relevant agent(s) _No response_ ### Additional context _No response_

Comments

epilande

Reproduced this on macOS with `fence` and dug into where detection actually breaks. The parent-pid theory isn't it: ccmux runs one global `ps` and joins matched processes to panes by tty, and the fallback tree walk is unbounded depth. `fence -- pi` and `fence -t code -- agy` both show up as pane-tracked sessions at the grandchild pid, so wrapper nesting on its own is fine. What I measured with Claude: - `fence -t code -- claude`: works. The hook can't run `ps` inside the sandbox so the marker lands with `tty: "unknown"`, but the daemon's PID fallback binds it to the pane. The row only appears after the first prompt is submitted, because Claude doesn't write its transcript until then. That's the same as unsandboxed Claude with hooks installed. - `fence -- claude` with the default profile: fence denies writes to `~/.claude` and `~/.config/ccmux`, so Claude has no transcript and ccmux has no marker. No row ever, but Claude itself is showing EPERM errors in that mode. Claude is the one agent where a missing marker means no row at all: with hooks installed, ccmux turns off pane-tracked fallback for Claude to avoid double-tracking. Every other agent degrades to terminal-pattern tracking and still appears. The cases I couldn't test here and suspect you're hitting: - Linux with a wrapper that calls `setsid` (`bwrap --new-session` does this). `ps` then reports `?` for the tty and ccmux drops the process before any agent matching runs. That would hide pi and agy too. - Docker or a VM. The agent's pty belongs to the container and its parent chain doesn't lead back to the pane, so there's nothing for ccmux to join on. To narrow it down: 1. Which OS, and the exact wrapper command you're running? 2. Which agent(s) disappear? Does `fence -- pi` show up while `fence -- claude` doesn't? 3. Did `ccmux setup` install hooks for that agent? 4. For Claude, had you sent a prompt yet when you checked? On `ccmux agent register`: herdr needs `HERDR_AGENT` because its detector looks at the pane's foreground process and sees `fence`. ccmux already sees the real `claude` process, so a manual registry wouldn't reach any of the failure points above. If it's the Linux tty case, the fix is daemon-side (fall back to fd or ancestry discovery instead of dropping `?`-tty rows), which I can do once I know that's what you're seeing.

eleith

your questions operating system: ubuntu 24.04 (popos) wrapper: nono -p claude -- claude setup: hooks were properly installed for claude prompt: yes, i sent in a prompt when i run nono, there are two processes that can be found (a) the parent nono process wrapper and (b) the child which is the agent itself running from what i can tell, when i first send in a prompt, the PID gets captured by the hook and written in a file. then that file gets removed. my assumption was the PID was seen as not matching the PID of a claude agent. happy to look into a log if you had more questions.

epilande

Thanks, that narrowed it down. I reproduced this on Linux with nono, and it's fixed in two stacked PRs. **What happens with nono.** nono forks and runs Claude on a pty it allocates, so the `claude` process sits on a different tty than the tmux pane (`nono` on the pane's pts, `claude` on its own). ccmux joined processes to panes by tty everywhere, so the agent could never be placed in a pane, and a hooked Claude row that can't be placed never shows up. The hook's side was fine: with a profile that allows writes to `~/.config/ccmux`, the marker lands with the correct pid (the `claude` child, not the wrapper) and Claude's real tty. The daemon just had no pane whose tty matched it. **A second, unrelated Linux problem** that may also have been hiding agents for you: ccmux still shelled out to `lsof` on Linux to read each process's working directory. On a box without `lsof`, every process comes back without a cwd and the daemon tracks nothing at all, sandboxed or not. **The fixes** - #194 reads the cwd from `/proc/<pid>/cwd` on Linux and drops the `lsof` dependency. - #195 (stacked on #194) adds a process-ancestry fallback to every pane-binding path: when no pane's tty matches, the agent binds to the pane whose shell is its ancestor. Verified on Linux with the stock `nolabs-ai/claude` pack (marker write denied, binds through the transcript) and with a user profile that allows the marker write (your setup): the row appears bound to the right pane a few seconds after the first prompt. With a plain Claude and a nono Claude side by side in the same directory, each binds to its own pane. On `main` and on #194 alone the same run produces no row at all. One thing I could not reproduce is the marker being removed. Across all three builds the marker stayed until Claude exited. The daemon only deletes a marker when its pid drops out of process discovery on two consecutive scans, or when that pid's tty no longer matches the marker. If the row still doesn't show for you after #195, could you paste this while Claude is running so I can see which of those is happening? ``` cat ~/.config/ccmux/session-pids/claude-*.json ps -eo pid,ppid,tty,comm | grep -E 'nono|claude' ``` Keep the `~/.config/ccmux` allow rule in your profile. With the marker present, ccmux gets per-turn status from the hook instead of waiting on the transcript.

epilande

This has been released with 🔖 [**v1.4.1**](https://github.com/epilande/ccmux/releases/tag/v1.4.1)

eleith

confirmed! it works.