The history layer, and the last change that could be made without a live member.
claude-sessions.ts had `claudeHome = process.env.HOME_DIR ?? join(DATA_PATH,
email, 'home')`, which discards its argument whenever HOME_DIR is set — always,
on a real install. Every transcript read therefore resolved to the OWNER'S
~/.claude no matter who asked, and the comment above it asserted "single-user
platform" as though that were a property rather than an assumption. A member
reaching these functions would have been handed the owner's conversation list.
Now every read takes a ChatIdentity {email, home} with the home resolved from
resolveHomeDir(userId), and this file has no way to invent one. Both fields
travel together because they are genuinely different: general_chat_sessions
lives under DATA_PATH/<email>, not under a home. Collapsing them would be the
same class of mistake as undefined meaning "the owner".
websocket.ts's resolveCwd takes a home, so `~` expands against the caller's own.
Identity is resolved BEFORE the cwd — expanding `~` before knowing whose home it
is would be exactly the bug being removed — which also let a duplicate
resolveTurnIdentity call from 6aeb304 be deleted.
chat.ts resolves per request and throws FORBIDDEN rather than falling back, same
posture as resolveTurnIdentity. agent-runner passes the owner's home explicitly
rather than inheriting it, since that path really is owner-only.
Made at 01:00 after saying it should not be. Three things I am least sure of are
listed in COMMS 29 rather than left for the reviewer to find: chat routes now
have a failure mode they did not have, resolveBaseCwd's exported parameter
changed meaning rather than shape, and the bare-email rewrite in chat.ts was
mechanical with hand repair.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3.2 KiB
29 — the history layer is off getOwnerHomeDir
The owner asked me to carry on, so 27's "tomorrow" became tonight. This is the change I said I would not
make at this hour, made at this hour — review it accordingly, and I would rather you were slow about it than
quick.
What moved
claude-sessions.ts no longer knows how to invent a home. claudeHome(email) is gone. Every transcript
read now takes a ChatIdentity { email, home }, and the home arrives resolved from resolveHomeDir(userId).
The old line was process.env.HOME_DIR ?? join(DATA_PATH, email, 'home') — which discards its argument
whenever HOME_DIR is set, i.e. always on a real install. So every read resolved to the owner's ~/.claude
regardless of who asked, and the comment above it asserted "single-user platform" as though that were a
property rather than an assumption.
Both halves travel together rather than one standing in for the other, because they are genuinely different:
general_chat_sessions lives under DATA_PATH/<email>, not under a home, so those paths stay email-derived.
Collapsing them would have been the same class of mistake as undefined meaning "the owner".
chat.ts resolves the identity per request (chatIdentity), and throws FORBIDDEN rather than falling
back — same posture as resolveTurnIdentity, for the same reason.
websocket.ts's resolveCwd takes a home rather than an email, so ~ expands against the caller's own
home. Its old comment — "the server owner is the only account, absolute paths are theirs to use" — is
replaced with what is actually true now: an absolute path still passes through, and it is the kernel that
decides what a member's turn can open, not a string check here.
Ordering worth checking: identity is resolved before the cwd, because expanding ~ before knowing whose
home it is would be exactly the bug this removes. That reordering also let me delete a second
resolveTurnIdentity call that 25 had left further down the same function.
agent-runner.ts passes { email, home: homeDir } explicitly — it is an owner-only path, and I would
rather it state that than inherit it.
What I am least sure of
chatIdentitythrows where the old code could not fail. Every/chatroute now has a failure mode it did not have. Owner-only today, andresolveHomeDirreturnsisOwnerfrom a positive check — but it is the kind of change that turns a database blip into a 403 on a screen that used to always load.resolveBaseCwdis exported. I changed its parameter from email to home and followed the compiler; if anything outside this diff passes an email,tsgowould have caught it, but the meaning of a wrong-but- well-typed string would not be caught by anything.- The regex pass. I rewrote bare
emailreferences insidechat.tsmechanically and then repaired the damage by hand.tsgois clean and 78 tests pass, but that is a weaker guarantee than usual for this file.
tsgo clean, 78 tests, both gates up, member unreachable in production.
What is left
Nothing I can write blind. deprovisionOsAccount is still mine and still tomorrow's. A member signing in, the
first member turn, and the gates are the owner's.