fix: a new account's first Claude session no longer opens on the bypass warning

--allow-dangerously-skip-permissions (bypass in the pane's shift+tab cycle)
made Claude Code show its Bypass Permissions warning on any account that had
not accepted it yet — every new member's first session, which the Chat UI
cannot answer, though nobody asked for bypass. Now passed only when the run
starts in bypassPermissions (asking is consent to see it) or the account's
user settings already hold skipDangerousModePermissionPrompt (pty-run
bypassAccepted; a member's read owner-checked from their home). The host,
which accepted long ago, keeps the flag. Test: pty-run.
This commit is contained in:
2026-10-12 14:52:52 +07:00
parent c47dd230d4
commit 8ecb96e9fc
2 changed files with 50 additions and 2 deletions
+21 -1
View File
@@ -374,7 +374,27 @@ describe("pty-run", () => {
if (args[0] === "new-session") newSessionArgv = args; if (args[0] === "new-session") newSessionArgv = args;
return ""; return "";
}); });
pty.spawnRun({ laneId: 4, cwd: "/tmp/repo" }); const prevHome = process.env.CLAUDE_HOME;
const home = fs.mkdtempSync(path.join(os.tmpdir(), "bypass-home-"));
process.env.CLAUDE_HOME = home;
try {
// An account that never accepted the bypass warning: no flag, so its
// first session does not open on that warning (the Chat UI can't answer).
pty.spawnRun({ laneId: 4, cwd: "/tmp/repo" });
assert.ok(!newSessionArgv.includes("--allow-dangerously-skip-permissions"));
// ...unless the run asks for bypass itself: then the warning is wanted.
pty.spawnRun({ laneId: 4, cwd: "/tmp/repo", permissionMode: "bypassPermissions" });
assert.ok(newSessionArgv.includes("--allow-dangerously-skip-permissions"));
fs.writeFileSync(
path.join(home, "settings.json"),
JSON.stringify({ skipDangerousModePermissionPrompt: true })
);
pty.spawnRun({ laneId: 4, cwd: "/tmp/repo" });
} finally {
if (prevHome === undefined) delete process.env.CLAUDE_HOME;
else process.env.CLAUDE_HOME = prevHome;
fs.rmSync(home, { recursive: true, force: true });
}
assert.ok(newSessionArgv.includes("--allow-dangerously-skip-permissions")); assert.ok(newSessionArgv.includes("--allow-dangerously-skip-permissions"));
// ...as an OPTION only: the starting mode stays the requested one, and // ...as an OPTION only: the starting mode stays the requested one, and
// `--dangerously-skip-permissions` (which would enable it) is never passed. // `--dangerously-skip-permissions` (which would enable it) is never passed.
+29 -1
View File
@@ -165,6 +165,24 @@ function promptFileArgv(id, argv) {
return ["sh", "-c", READ_PROMPT, "sh", file, ...argv.slice(0, -1)]; return ["sh", "-c", READ_PROMPT, "sh", file, ...argv.slice(0, -1)];
} }
/**
* Whether the account a pane runs as has accepted Claude Code's bypass
* warning — Claude writes `skipDangerousModePermissionPrompt: true` to its
* user settings once it is. A member's (S3) is read from their home, owner-
* checked; the host's from CLAUDE_HOME. Unreadable reads as "not yet".
*/
function bypassAccepted(member) {
const file = member
? path.join(member.home, ".claude", "settings.json")
: path.join(require("./claude-home").getClaudeHome(), "settings.json");
try {
const text = require("./run-as").readFileSafe(file, "utf8", { max: 1 << 20 });
return JSON.parse(text).skipDangerousModePermissionPrompt === true;
} catch {
return false;
}
}
function buildClaudeArgv({ function buildClaudeArgv({
model, model,
permissionMode, permissionMode,
@@ -177,6 +195,7 @@ function buildClaudeArgv({
addDirs = [], addDirs = [],
systemPromptFile = null, systemPromptFile = null,
proxyArgv = [], proxyArgv = [],
offerBypass = true,
platform = process.platform, platform = process.platform,
}) { }) {
const argv = ["claude"]; const argv = ["claude"];
@@ -210,7 +229,11 @@ function buildClaudeArgv({
// plan/auto/manual/acceptEdits, so a run started in any non-bypass mode could // plan/auto/manual/acceptEdits, so a run started in any non-bypass mode could
// never reach bypass from inside the pane, while a real terminal could. // never reach bypass from inside the pane, while a real terminal could.
// The starting mode is still whatever --permission-mode says above. // The starting mode is still whatever --permission-mode says above.
argv.push("--allow-dangerously-skip-permissions"); // Only once the account has accepted Claude Code's bypass warning
// (`offerBypass`, see bypassAccepted): before that the flag alone puts the
// warning in front of a brand-new account's first session — one the Chat UI
// cannot answer — although nobody asked for bypass.
if (offerBypass) argv.push("--allow-dangerously-skip-permissions");
if (model) argv.push("--model", model); if (model) argv.push("--model", model);
if (effort && EFFORT_LEVELS.has(effort)) argv.push("--effort", effort); if (effort && EFFORT_LEVELS.has(effort)) argv.push("--effort", effort);
if (resumeSessionId) argv.push("--resume", resumeSessionId); if (resumeSessionId) argv.push("--resume", resumeSessionId);
@@ -512,6 +535,11 @@ function spawnRun(args) {
resumeSessionId, resumeSessionId,
forkSession: forkSession === true, forkSession: forkSession === true,
newSessionId, newSessionId,
// Asking for bypass is consent to see its warning; otherwise only an
// account that already accepted it gets bypass in its shift+tab cycle.
offerBypass:
permissionMode === "bypassPermissions" ||
(entry.launcher === "claude" && bypassAccepted(require("./run-as").accountForLane(laneId))),
worktreeName: worktree === true ? `ah-${laneId}-${Date.now().toString(36)}` : null, worktreeName: worktree === true ? `ah-${laneId}-${Date.now().toString(36)}` : null,
resumeLast, resumeLast,
bypass: bypass === true, bypass: bypass === true,