# Row 68 (#1561): the bus host runs an explicit Claude Code binary, round 1 Sage, 2026-10-11. Brief: `docs/plans/2026-10-11_bus-claude-binary.md`, "Bus host runs an explicit Claude Code binary" (e1b90ed6). Mos's Q18 ask: the bus finds `/usr/bin/claude` 2.1.259 (pacman `claude-code`), which refuses `claude-opus-5-5`. Jason runs the native `~/.local/bin/claude` (2.1.296). Filbert and Dewey review. Sage commits after the gate; nothing is committed, staged or pushed yet. Base a86de1f1. `build.patch` applies there with `git apply --index`; `candidate-manifest.sha256` pins all 13 files after the patch. ## Design The binary is named, not found. `MOSAIC_CLAUDE_BIN` is an absolute path: 1. The unit sets it. `scripts/bus-service.sh render|install --claude PATH`; without `--claude`, `command -v claude` in the installing shell. The path is kept as given, not resolved: `~/.local/bin/claude` is a symlink the native installer moves to each new version, so the unit follows updates without a reinstall. 2. `ENV_ALLOW` passes it to the session. It's a path, not a credential. 3. `adapters/claude/adapter.sh` runs `$MOSAIC_CLAUDE_BIN` when it is set and nonempty, else `claude` as before. 4. `claudeVersion`, which fills the manifest's `claudeVersion`, asks the same binary, so the manifest records what actually ran. Rejected: prepending `~/.local/bin` to the unit's `PATH`. Every binary there (pip and npm installs, personal scripts) would then shadow the system's, for the host and for every session. Also rejected: a field in the business file or system config. Which binary runs is a property of the host install, like node's directory, which the unit already carries. ## Refusals | Where | Input | Result | |---|---|---| | `bus-service.sh` | `--claude` relative | exit 1, "must be absolute", nothing written | | `bus-service.sh` | not an executable file, or missing | exit 1, "not an executable file" | | `bus-service.sh` | a character outside `A-Za-z0-9/._+-` (`%`, space, ...) | exit 1, "cannot carry". systemd expands `%` and splits `Environment=` on spaces. | | `bus-service.sh` | no `--claude` and no claude on `PATH` | exit 0; the unit gets `# MOSAIC_CLAUDE_BIN unset: ...` and stderr says so. A pi-only host installs as before. | | `bus-service.sh` | `uninstall --claude`, `render --dir` | exit 4 | | adapter | `MOSAIC_CLAUDE_BIN` relative | exit 2 before claude runs | | adapter | not an executable file, or missing | exit 2 before claude runs | | adapter | unset or empty | `claude` from `PATH`, as today | `install` now renders into a variable before `mktemp`, so a refused pin leaves no temp file in the unit directory. Calling `render` inside `|| { ... }` would have switched `set -e` off in the function and written a unit with an empty line. `claude_line` failing is handled with an explicit `|| exit 1`. `install` prints `claude: (<--version>)` or the unset line. `status` adds `-p Environment` to its `systemctl show`, which shows `PATH` and the pin. The unit holds no secret. ## Files - `packages/cli/systemd/mosaic-bus.service.in`: a comment and an `@CLAUDE_ENV@` line after `Environment=PATH=`. The header names "the claude line below" rather than the token, because header tokens render into the comment (as `@REPO@` and `@PATH@` already do) and the claude substitution only matches its own line. - `scripts/bus-service.sh`: `claude_line`, `--claude` for render and install, the install print, `status` shows `Environment`, usage. - `adapters/claude/adapter.sh`: the `CLAUDE` variable and its checks. - `packages/seat/src/session.mjs`: `MOSAIC_CLAUDE_BIN` in `ENV_ALLOW`. - `packages/cli/src/launcher.mjs`: `claudeVersion` asks `env.MOSAIC_CLAUDE_BIN || "claude"`. - Docs: `packages/cli/README.md` ("Unit" and the manifest note), `adapters/README.md` (adapter list and variable table), `packages/seat/README.md` (`sessionEnv`), `docs/TOOLS.md` (usage line and one paragraph). The brief didn't list `docs/TOOLS.md`. I added it because its usage line would otherwise be wrong, and it is my file. - Tests: - `host.test.mjs`, new test "bus-service.sh pins the claude binary...": - the pin found on `PATH`, from `--claude`, and through a symlink kept unresolved - five refusals with exit 1 and empty stdout - install printing the pin and the stand-in's version - a refused install writing nothing - `uninstall --claude` and `render --dir` exiting 4 - no claude anywhere: `PATH` is a directory holding only node, dirname and sed, and the unit gets the comment line - The existing render test also asserts no `@CLAUDE_ENV@`. - `launcher.test.mjs`: `claudeVersion` with the pin, from `PATH`, and with none (null). - `session.test.mjs`: `sessionEnv` passes `MOSAIC_CLAUDE_BIN`. - `claude-session.test.mjs`, new test: - the pin wins over a `claude` on `PATH` - unset or empty uses `PATH` - relative, missing and non-executable pins exit 2 without running anything Each new test fails against base: unpinned `claudeVersion` returns the `PATH` stand-in's 1.0.0, the base adapter runs the `PATH` stand-in, base `sessionEnv` drops the variable, and base `render` rejects `--claude` with exit 4. ## Gate (my run on the candidate) | Suite | Result | |---|---| | packages/cli | 85/0 | | packages/seat | 27/0 | | packages/harness | 57/0, nothing skipped. The live claude tests ran against the 2.1.296 on my PATH. | | scripts/test-*.sh | all nine exit 0: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 14, task 98 (Docker on) | Logs: `/tmp/sage-r68-out/` (not kept). ## After landing (not in the candidate) 1. `scripts/bus-service.sh install` from my shell, which resolves `/home/jwoltje/.local/bin/claude`. `install` prints the pin and its version. 2. The opus probe: the adapter's flags with `MOSAIC_CLAUDE_BIN` and `PATH` taken from the installed unit, as decision 89's probe did. 3. Report the path and version to Mos. The running host (MainPID 1242258) keeps its old environment until it restarts. That restart is the Q18 `--pm` start, which waits for Mos's watched window. Nothing here restarts it. ## Questions for reviewers - Is keeping the symlink, not its target, right? The other choice pins an exact version and needs a reinstall per update. I chose to follow updates because that's what Jason's own sessions do. - With no claude found, `install` succeeds with a comment rather than refusing, because a pi-only business needs no claude. Is that the right default, or should `install` refuse unless the business's roles are all pi?