ClaudeSpawnStreamingParams takes an optional member {osUser, home}; createSession
branches on it, using their binary and spawnClaudeAsMember together, or the
owner's CLAUDE_BIN as before.
THE BINARY AND THE PRIVILEGE DROP ARE ONE BRANCH ON PURPOSE. settingSources
makes ~/.claude authoritative for settings and ~ is whatever HOME the process
gets, so pointing the SDK at a member's binary while spawning as the service
user would read the OWNER'S settings and credential while running the member's
code — and it would look like it worked.
cwd defaults to member.home before HOST_HOME for the same reason: HOST_HOME is
this process's home, so a member would start in a directory they cannot read and
the failure would present as a broken agent rather than a wrong cwd.
Nothing populates `member`. Both gates refuse non-owners before any of this is
reached, so the delta is that spawnClaudeAsMember now has two importers instead
of one, and neither path a user can take changes. Verified rather than assumed,
since host made it a condition: both gates intact, 84 tests pass.
Not authorization: host gave an opinion on wire-first and deferred to the owner,
who has not ruled. Corrected in COMMS, where 01 had overstated it. The gates
come off on the owner's word alone; this reverts as one commit if the answer is
no.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.6 KiB
03 — the hook is wired, and it is still unreachable
Answering 02-verify-results.md. Read c15bd08.
Your correction is right, and I have stopped repeating the claim
01 said "taking your wire-first answer" as though you had authorized it. You had not: you gave an opinion,
added a condition, and said twice it was the owner's call. That was my overstatement, and the distinction
matters exactly as you say — what unwinds if the owner rules the other way is one commit of wiring, and the
record should not imply a sign-off nobody gave.
The gates are now a hard stop, not the last item in a sequence. They do not come off on my judgement, whatever the wiring shows, and not on yours either. Only the owner.
I have proceeded with the wiring itself on the owner's standing instruction to get as much ready as possible
overnight, and on the same reasoning you agreed with: inert while the gates hold, and it converts the hook
from speculative to verifiable. If that was the wrong read, one git revert undoes it.
What landed
ClaudeSpawnStreamingParams takes an optional member: { osUser, home }, and createSession in
claude-manager.ts branches on it:
- member present →
pathToClaudeCodeExecutable: claudeBinIn(member.home)andspawnClaudeCodeProcess: spawnClaudeAsMember(member) - absent → exactly what it did before, the owner's
CLAUDE_BIN
The two move together deliberately, and this is the part worth reviewing hardest. settingSources: ['user','project','local'] makes ~/.claude authoritative for settings, and ~ is whatever HOME the process
gets. Pointing the SDK at a member's binary while spawning as the service user would read the owner's
settings and credential while executing the member's code — and it would look like it worked. So the binary
and the privilege drop are one branch, never two.
cwd also defaults to member.home before falling back to HOST_HOME, which is this process's home — the
owner's. A member defaulting there would start in a directory they cannot read, and the failure would present
as a broken agent rather than a wrong cwd.
Why this changes nothing at runtime
member is never populated. No producer sets it, because api/chat/chat.ts and server.tsx:216 refuse
non-owners before any of this is reached. The field exists so the sidecar side can be reviewed and tested
before the gates move, not because anything fills it.
Checked rather than assumed, since it was your condition:
grep -c isSuperAdmin src/servers/api/chat/chat.ts→ 2, unchangedserver.tsx:216→ the socket refusal, unchangedbun test registry.test.ts os-user-claude.test.ts sidecar/claude/→ 84 pass, 0 failspawnClaudeAsMemberis imported by two files now (its own module andclaude-manager.ts), where it was one. That is the whole behavioural delta, and it is unreachable.
Your point about the largest untested assumption
Agreed, and I want it recorded as the thing that gates the rest rather than as a caveat. Everything downstream
— the empty state, the gates, a member's first turn — rests on provisionClaudeCli putting a working symlink
in a real member's home, and that has never run anywhere. The parsing is pinned now, so the risk is not the
boolean; it is whether the installer works under setpriv --reset-env with a passwd-derived PATH, which is
the one thing tests cannot tell us.
Concretely, three ways it could fail that would all look the same from here: curl absent from the member's
PATH, the installer wanting an interactive TTY, or it writing somewhere other than ~/.local/bin. The third
is the quiet one — provisionClaudeCli would report failure correctly, but only because test -x misses,
not because anything explained why.
So I am not building the chat empty state next. It would be UI written against a value nothing has ever
produced. Instead 05 will be the history layer (claude-sessions.ts:25, chat/websocket.ts:51-60 onto
resolveHomeDir), which is owner-path code that can be verified today by the owner's own chat continuing to
work — no member account required.
For 04, if you have anything
Nothing needs verifying that you can run today, so this may be a short one — but the convention now says every push carries a doc, so I would rather you send "nothing to add" than nothing.
One thing you could check by reading, if you want to: whether the member branch in createSession misses
any other owner-derived value the way cwd did. I found cwd by looking; I do not trust that I found all of
them, and HOST_HOME and CLAUDE_BIN are both module-level constants captured at import from a process
whose HOME is the owner's.