fix: v2.7.4 — a developer's System page carries no host or other-user path

- GET /api/settings/info for a developer: no DB path, no hooks file path,
  and no transcript-cache keys — those were EVERY user's transcript paths
  (their folders and session ids). GET /api/updates/status: no repo_root.
  Test: server-mode-leaks.
- Also in this release (earlier commits): a member's missing workspace
  folder is made as them at start; --allow-dangerously-skip-permissions only
  for an account that accepted the bypass warning or a bypass run.
Full suites: server 2889/2889, client 1250/1250, tsc -b.
This commit is contained in:
2026-10-12 15:07:21 +07:00
parent 8ecb96e9fc
commit b3fe10a4f3
6 changed files with 48 additions and 9 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Claude Code Working Guide
_Last synced: v2.7.4 (2026-10-12) — Changes hides a staged change undone on disk (#72); chat says when the session has not started (#73)._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session.
_Last synced: v2.7.4 (2026-10-12) — Changes hides a staged change undone on disk (#72); chat says when the session has not started (#73); a member's missing folder is made as them, bypass offered only once accepted._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session.
## Project mission
- Maintain a reliable local-first dashboard for Claude Code session monitoring.
+1 -1
View File
@@ -359,7 +359,7 @@ Error `409 ESESSIONLIVE` is returned when `resumeSessionId` (or `resume`) names
Error `400 EBADPROMPT` is returned for a `prompt` (or `POST /api/run`'s `initialPrompt`) over 128 KB: no CLI can take one argument that long (Linux's `MAX_ARG_STRLEN`, issue #68) — attach the text as a file instead. A prompt over 4 KB never rides in tmux's argv (one tmux message holds 16 KB, `command too long`): outside Windows it is written to `<data dir>/run-prompts/<run id>.txt` (0600) and the pane's own `sh` reads it back as the CLI's last argument and deletes the file. An `after-start` tool's paste still goes through `tmux set-buffer` and stays under that 16 KB.
Error `409 ENOCWD` is returned when the workspace's folder no longer exists (or is not a directory) — the pane would otherwise start in the account's home under this workspace's name. A folder this server cannot read (`EACCES`) is not refused.
Error `409 ENOCWD` is returned when the workspace's folder no longer exists (or is not a directory) — the pane would otherwise start in the account's home under this workspace's name. A folder this server cannot read (`EACCES`) is not refused. Server mode with one Linux user per account: a missing folder inside a member's own home is created instead, as that member (`mkdir -p` through `write-as`) — their home itself can be no workspace, so this is how a new member's first project folder comes to exist.
Error `409 ERUNLIVE` is returned if the workspace already has a live run; stop it first with the `stop` action. The same code and status come back when the workspace's tmux pane is already running an agent while a `prompt` or `resumeSessionId` was sent — that argv cannot be typed over a live agent, so the request is refused instead of answered 200 with nothing done. A start that asks for neither still adopts the existing pane silently.
+14 -2
View File
@@ -822,8 +822,20 @@ agenthub users provision alice # the Linux account only (a user made before
writes three `auth.log` lines); a pane the dashboard starts or stops shows at
once, one that exits on its own within those 5 s.
- Claude Code does not carry a folder's trust down from the member's home
(which provisioning trusts): a member answers "Trust this folder" once per
workspace, in the terminal.
(which provisioning trusts): a member may be asked "Trust this folder" once
per workspace, in the terminal. AgentHub never answers it for them — it is
what keeps a cloned repository's own settings and hooks from running unasked.
- A member's first project folder: a workspace named on a folder that does not
exist yet inside their home is created at its first start, AS the member
(from v2.7.4; their home itself is no workspace).
- Bypass in a pane's shift+tab cycle (`--allow-dangerously-skip-permissions`)
is offered only to an account that already accepted Claude Code's bypass
warning (`skipDangerousModePermissionPrompt` in its `~/.claude/settings.json`)
or to a run that starts in bypass itself. Before v2.7.4 every new member's
first session opened on that warning, which the Chat UI cannot answer.
- A developer's System page (`GET /api/settings/info`, `GET /api/updates/status`)
carries no path: not the database's, the hooks file's, the install folder,
nor the transcript cache's keys — those are every user's transcript paths.
### Undo and uninstall
@@ -148,3 +148,23 @@ describe("server mode: a member's skills/commands come from their own workspaces
assert.ok(names(r).includes("max-skill"));
});
});
describe("the System page's facts for a developer", () => {
// Every string that looks like an absolute path, anywhere in a body.
const paths = (o, out = []) => {
if (typeof o === "string" && o.startsWith("/")) out.push(o);
else if (o && typeof o === "object") for (const v of Object.values(o)) paths(v, out);
return out;
};
it("carries no host or other-user path (DB, hooks file, transcript cache keys)", async () => {
const mia = await login("mia", "member-pass-1");
const info = await call("/api/settings/info", mia);
assert.equal(info.status, 200);
assert.deepEqual(paths(info.body), []);
assert.equal(typeof info.body.db.size, "number", "the numbers stay");
const boss = await login("boss", "admin-pass-1");
const full = await call("/api/settings/info", boss);
assert.ok(full.body.db.path, "an admin still sees the host's paths");
});
});
+9 -4
View File
@@ -35,7 +35,7 @@ const {
importExportBundle,
exportableRun,
} = require("../lib/data-transfer");
const { sessionWhere, bySessionWhere } = require("../lib/scope");
const { sessionWhere, bySessionWhere, memberId } = require("../lib/scope");
function getDbSize() {
try {
@@ -127,15 +127,20 @@ router.get("/info", (req, res) => {
h1: getCount(60 * 60 * 1000),
};
// A developer (server mode) gets the numbers, never a path of the host's:
// the transcript cache is keyed by EVERY user's transcript path (their
// folders, their session ids), and the DB/settings paths are the host's.
const developer = memberId(req) != null;
const cache = transcriptCache.stats();
res.json({
db: {
path: DB_PATH,
path: developer ? null : DB_PATH,
size: dbSize,
counts,
pragmas,
load_stats,
},
hooks: hookStatus,
hooks: developer ? { installed: hookStatus.installed } : hookStatus,
server: {
version: APP_VERSION,
uptime: process.uptime(),
@@ -152,7 +157,7 @@ router.get("/info", (req, res) => {
free_mem: os.freemem(),
cpus: os.cpus().length,
},
transcript_cache: transcriptCache.stats(),
transcript_cache: developer ? { ...cache, keys: undefined } : cache,
});
});
+3 -1
View File
@@ -24,9 +24,11 @@ const { schedulePluginUpdate } = require("../lib/plugin-update");
const router = Router();
router.get("/status", async (_req, res) => {
router.get("/status", async (req, res) => {
try {
const status = await getUpdatesStatus();
// The install folder is the host's; a developer sees versions only.
if (require("../lib/scope").memberId(req) != null) status.repo_root = null;
res.json(status);
} catch (err) {
res.status(500).json({