Repository navigation
Inbox launch: answer Claude Code permission prompts in the thread (stack 2/4) - #2378
Open
backnotprop wants to merge 42 commits into
Open
backnotprop wants to merge 42 commits into
backnotprop wants to merge 42 commits into
Conversation
A per-launch PermissionRequest hook (plannotator inbox permission-hook) asks the person in the launch thread under the perm:<session> identity, long-polls /api/inbox/launch/<id>/permission, and prints allow once, allow for this session, or deny with a reason. ExitPlanMode passes straight through. A 60 s lease withdraws the question when the prompt is answered in Terminal or no longer waited on. The run line gains waiting_answer. Behind inboxLaunch.enabled. Co-Authored-By: Claude <noreply@anthropic.com>
…eat/inbox-launch-permissions
… smoke Co-Authored-By: Claude <noreply@anthropic.com>
M1: a command's list lines can never become choices. The command and
folder now sit above the :::question block, which holds only the prompt
and three "- [ ]" choices; lines starting ":::" are escaped. A pick is
read by exact label. The route checks the stored question has exactly
this prompt's labels, and withdraws it otherwise (fail closed).
S1: the Inbox sends the hook only { choice: once|session|deny, reason }.
The hook builds the decision from its own event, so whatever answers on
the port cannot change tool_input or grant more than Claude suggested.
S2: picks/reply on a perm: question need the page's own Origin and this
run's serverSession (403 window_only otherwise). The perm: author prefix
is reserved to the permission route in the store. Docs say what this
does and does not protect against.
S3: "Answered in Terminal" only when no hook call is held.
S4: each hook call is cut off at 35 s; inbox.json is read again on each
retry; 15 refusals of one token end the wait with a stderr line.
Low: a command over 8,000 chars is never asked (Claude's dialog
decides); permission styling only in a launched thread with launch on;
turning launch off withdraws open questions; reuse the Inbox's
selfCommand; AskUserQuestion passes through like ExitPlanMode; refusals
log to stderr; dead code and the unused lease field are removed.
Co-Authored-By: Claude <noreply@anthropic.com>
N1: a path (Write, Edit, Read, NotebookEdit), a link (WebFetch) or a
query (WebSearch) is one line of inline code; one holding a line break,
a control or an invisible character is never asked (withdrawn, Claude's
own dialog decides). Every detail counts toward the 8,000 cap. The
route now checks the stored body, prompt and labels against the
question it would write now, so a reshaped card fails closed.
N2: the session label names every rule or folder the suggestion grants
("Allow `git add` and `git commit` for this session"); a rule it cannot
show exactly, or a label over 200 characters, offers no session choice.
N3.2: the store refuses a Re-ask on a perm: message (422), and the
window no longer offers one.
Phone: a request the device door hands in (its token checked) may
answer a permission question; a plain local call without the page's
Origin is still refused.
Low: bidi and invisible characters are shown as <U+202E> markers in
every detail and label, never hidden; asked() returns { prompt, detail }
and inboxPermissionQuestion decides askable; the stale "(after one more
host read)" is gone; the docs state the accepted risks (a local program
forging headers, browser automation clicking the window).
Co-Authored-By: Claude <noreply@anthropic.com>
…missions # Conflicts: # packages/inbox/api.ts
Builds approval "inbox-permission-prompt" (card frame and width, the dock's header) into the launch thread: - PermissionCard: header "Claude Code is waiting for your permission · N min" with Stop, in place of the separate run line; the action title and the exact request (command with purpose and folder, Write content as numbered lines, Edit as removed/added lines, WebFetch link with host and "Looking for"); Allow once (accent), Allow <rule> for this session, Deny, Deny with a reason; "Or allow this and switch the session to Auto mode · Full permissions". Answered, it shrinks to one framed line; the answer reply and the withdrawal follow-up are hidden. - Mode switches are real: two new exact-label choices, enum "auto" | "full"; the hook answers allow with setMode auto / bypassPermissions built from its own event. Full permissions only for a launch with full permissions (inboxLaunchFullPermissions, the one seam for the open question) and only when the hook's own env has PLANNOTATOR_LAUNCH_FULL_PERMISSIONS=1; otherwise drawn off with its reason. Confirms the first time per session. - Allow once takes focus when the card is newest and waiting, and the footer is scrolled into view, so Enter answers it. No new shortcuts. - Body additions (Write content, Edit diff, Bash description, WebFetch purpose) go through the same fencing and escaping; the stored body is still compared with the rebuilt one (fail closed). - The questionChrome seam in packages/ui is removed again (packages/ui equals main). Tests: escaping of a Write holding fences and "- [ ]" lines; mode-switch choice, route and hook tests; e2e of the approved card through the real hook (Write by Allow once with Enter, Bash by the session rule, WebFetch denied with a reason, Edit by Auto mode with setMode auto, Full permissions off and confirmed). Co-Authored-By: Claude <noreply@anthropic.com>
- The session button wraps, so every rule it grants shows on desktop and phone. - A WebFetch link is shown exactly as sent (scheme, host, path, query), wrapped, with "Host: ..." on its own line when the text does not start with the real host. - Allow once takes focus only from the page body, without a focus ring; the answers ignore activation for 600 ms after the card appears or takes focus, and a repeated keydown never clicks. - Full permissions has one source, the launch record, for the start flags, the hook's env and the card. - Picks on a permission question are unsent-only in the store; the first Full permissions pick of a session needs confirm_full on the server too. - The request in full: Write says create or overwrite, Edit shows replace_all, MultiEdit and NotebookEdit show their edits, Bash shows "Runs outside the sandbox", and undrawn input keys show as JSON. - Auto and Full records say what was asked, never that it switched. - Session rule labels keep their backticks (code spans parsed properly). - More invisible ranges shown (tag block, soft hyphen, Hangul fillers, variation selectors); doc comments fixed; disabled Full permissions note in flow, never over Deny. Co-Authored-By: Claude <noreply@anthropic.com>
…sync file check
- A reply carrying its own answer for an answered permission question posts its
words as text and leaves the answer record alone (question_already_sent when
nothing else is left to send).
- The Write overwrite check is an async lstat given 300 ms, so an unreachable
network path never freezes the Inbox; a timeout or error says "Write a file".
- A file tool's path that is a symbolic link shows where it points
("Links to ..."), kept the same when the question is checked again.
Co-Authored-By: Claude <noreply@anthropic.com>
…us keep one line The answered record's path takes only the room left and truncates first; the waiting header reads "waiting · N min" on a phone. The e2e checks both at 390 wide. Co-Authored-By: Claude <noreply@anthropic.com>
…uto mode - Never set, Settings -> Agents' "Start new sessions with full permissions" follows the login shell: CLAUDE_CODE_EVEN_MORE_DANGEROUSLY_SKIP_PERMISSIONS set to a true value makes the launch full (launch.full_permissions, still the one source, fixed when the launch is made; the first start reuses that read). Once the person sets the switch, their choice wins either way. Settings shows it on with "Follows your shell's setting until you change it." - Every launch starts with --allow-dangerously-skip-permissions and --permission-mode bypassPermissions (full) or auto. Auto mode refused: started again with no --permission-mode, and why goes to the start log. - The card always offers Full permissions; the greyed-out state is gone. - Fake CLI: asks nothing in bypass mode, refuses auto on refuseAuto, and its login shell takes extra exports. E2E for the shell default and the fallback. Co-Authored-By: Claude <noreply@anthropic.com>
… no hook in a full launch From the security review of c445a71: - M1/S1: at launch, read the person's own bypass consent (read-only) into launch.bypass_available. The allow flag, a bypass start and the card's Full permissions need it; older launches read as without it. The Inbox never writes consent. The fake CLI models Claude's consent rule. - S2: GET /api/inbox/launch/settings refuses another site's page; concurrent reads share one login-shell read. - S3: the auto fallback pins permissions.defaultMode "default" in that launch's settings file; only commander's refusal of the mode flag counts. - S4: a full launch has no PermissionRequest hook (Claude's skip export waives bypass's safety checks only without one); the Inbox tool stays allowed. - The order: the Settings switch, the person's own defaultMode (no --permission-mode, never forced to auto), the shell's export, then auto. - Nits: stale comments, the login env kept at most a minute; a card already there when the thread opens leaves Reply its focus. Co-Authored-By: Claude <noreply@anthropic.com>
… Claude reads it; Send off the shell From the re-check of 499db45: - M1: the Inbox PermissionRequest hook is in every launch, full included. Plannotator's plugin has its own PermissionRequest hook, and Claude then ignores the skip export, so bypass's safety checks still ask; they now reach a card. The export is no longer set anywhere. The fake CLI counts the enabled plugin's hook; e2e proves a full launch's safety check reaches the card. Docs no longer say a misjudged bypass fails to start. - S1: consent only from managed or user settings.json and ~/.claude.json; disableBypassPermissionsMode from managed, user settings.json and settings.local.json, and the launch folder's settings. - S2: every auto launch pins permissions.defaultMode "default". - S3: Send uses a shell read under a minute old and waits only to finish one under way, when the switch is unset and none is fresh, or when none ever finished; read once at Inbox start and ahead when the composer picks Start Claude Code; the start reads its own env after the response. - S4: a person's own mode applies whenever the launch is not full. - Nits: a Full pick a session cannot honour is Allow once; a full launch's card offers no Full; Claude files read once per settings request. Co-Authored-By: Claude <noreply@anthropic.com>
…read as Claude reads it From the review of 3e6006e: - S1: prepare decides from the last finished login-shell read, however old (each start reads the shell again); it waits only for a read under way or when none ever finished. - S2: disableBypassPermissionsMode counts only as "disable", from managed, the user's settings.json and the launch folder's settings (never ~/.claude/settings.local.json). Settings says plainly when bypass is turned off in Claude Code's settings (bypass_off), never the consent notice. - Nits: a full launch's card offers no mode switch; the Settings copy allows that a safety check still comes to the card; the stale env header and the docs on the ahead read are fixed. Co-Authored-By: Claude <noreply@anthropic.com>
…aunch's safety check is Deny or Terminal
Claude Code 2.1.314 ignores a PermissionRequest hook's allow for its own
safety checks (person-only) and keeps its Terminal dialog; a deny counts.
The hook input marks nothing, so the launch's mode decides.
- A full launch's card offers Deny and Deny with a reason, the host's note
("Claude Code only accepts this approval in Terminal."), Open in Terminal
(computer) and Copy attach command (everywhere).
- Every allow is checked: the first host read 3 s after the answer decides.
Still waiting on a permission prompt: a follow-up says Claude Code didn't
take it, and the record shows that with the Terminal actions; until then
"Sent, checking that it went through". Never "Allowed" before it took.
- Host reads from before an answer settled never show "waiting on a
permission prompt" for the prompt just answered.
- The thread read carries attach_command, so the resting menu has Copy
attach command (phones).
- Claude-only terms, note, follow-up text and the attach command live in
inbox-launch-claude.ts; core and the card take generic allowHere/note.
- Nits: Try again of a start that never ran is a first start (no "again");
no "· N min" under a minute.
- The fake CLI models person-only safety checks (allow ignored, own dialog,
auto-block after safetyBlockMs) and lists waitingFor while it waits.
Co-Authored-By: Claude <noreply@anthropic.com>
…safety checks to Claude's own dialog
Simplify after review: no allow-verification state machine.
- Removed the allow checks (settle/max timers, "Sent, checking", the
"didn't take" follow-up, run.approval_check). The record states the
person's own answer ("You allowed this once", "You denied this", "You
chose to approve this in Terminal"); the run line says what Claude does.
Host reads made before an answer settled still count for nothing, and one
read is due once it settled.
- M1: the hook input's permission_mode decides bypass mode
(claudePermissionTerms in inbox-launch-claude.ts); the launch record only
when the input has none. A full launch whose session says default keeps
Allow once.
- M2: a bypass card offers Deny, Deny with a reason and Approve in Terminal.
Approve in Terminal answers `terminal`: the hook prints nothing, so Claude
shows its own dialog (2.1.314 awaits the hooks first); on a computer the
card opens Terminal attached, the record says "Approve it in Claude Code
within 2 minutes", and on a phone Copy attach command until the run line
shows it. One set of Terminal actions.
- The bypass card's header says "is waiting on a safety check"; its question,
withdrawn with the hook gone, says "No longer waiting." (never "Answered in
Terminal.").
- The fake shows a safety check's dialog only after the hook ends, with its
own 2-minute deadline from then.
Co-Authored-By: Claude <noreply@anthropic.com>
…e click The real smoke on d8f8fae found a project permissions.ask rule reaching the card with Allow once: Claude ignored the allow and kept its own "Permission rule ... requires confirmation" dialog, while the card said "You allowed this once". The 2.1.314 binary builds the PermissionRequest input from the common fields, tool_name, tool_input, permission_suggestions and mcp_server: nothing marks a person-only request, so it cannot be detected. - Every card offers Approve in Terminal (a quiet link under the answers; the host's terms in inbox-launch-claude.ts), the honest way when Claude wants a person. It hands the prompt to Claude's dialog, as on the bypass card. - The record states the click: "You chose Allow once", "You chose Deny", "You chose Approve in Terminal". The run line says what Claude did. - The fake models an ask rule (askRule: person-only in any mode, allow ignored, dialog after the hook, no auto-block); a new e2e plays it. - Docs name the ask-rule case. Co-Authored-By: Claude <noreply@anthropic.com>
…eat/inbox-launch-permissions # Conflicts: # packages/server/inbox-launch-env.test.ts
scripts/launch-smoke/permission.ts is a manual smoke against the real claude, like day1.ts and claude.ts: it sandboxes the data dir, Inbox and login env, and its one real-home path is a read of the session transcript (permissionMode and whether the deny reason reached Claude). It writes nothing there. Co-Authored-By: Claude <noreply@anthropic.com>
…test's dialog settles first)
…eat/inbox-launch-permissions # Conflicts: # apps/hook/server/inbox-command.ts # docs/agent-guides/server-and-storage.md # packages/core/inbox-open-questions.ts # packages/inbox/api.ts # packages/inbox/components/OpenQuestions.tsx # packages/inbox/components/Thread.tsx # packages/inbox/footFit.ts
# Conflicts: # apps/inbox/index.css
# Conflicts: # docs/agent-guides/environment.md # tests/agent-cli-stubs.test.ts
# Conflicts: # docs/agent-guides/server-and-storage.md # packages/inbox/components/Thread.tsx # packages/server/inbox.ts # tests/agent-cli-stubs.test.ts
…es focus Main's cfd5a64 stopped focusing Reply when a thread opens, so the card's 'only from the page body' rule began taking focus on open. The thread now remembers the messages it opened with and only a card that arrived after may take Allow once's focus. The spec reloads with the prompt waiting to guard it.
# Conflicts: # apps/hook/server/inbox-command.ts
…, not crypto.randomUUID)
…en's first fresh read, never a stale copy App records the ids of the first thread read numbered after the open and passes them to ThreadPane (openedWith); until that read lands no card takes focus. Adds a DOM test for the focus rule (permissionFocus.test.tsx) that also covers the insecure-context answer.
The agent is blocked until the person answers, so a new permission question ends the thread's snooze at once (owner answer q-21cc8c10 A). A replayed hook call does not.
…aunch-permissions
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack 2/4 on #2376 (base
feat/inbox-launch-claude). Please don't merge: the stack stays open until the owner sees it working. Everything is still behindinboxLaunch.enabled. This PR keeps the flag; the owner decides when the stack ships.What it does
A Claude Code session started from the Inbox asks its permission prompts in its own thread. The person answers there: Allow once, Allow <Claude's suggestion> for this session, or Deny (with an optional reason). The Terminal dialog stays available too.
settings.jsongains onePermissionRequesthook,<plannotator> inbox permission-hook, with the longest timeout Claude allows. ExitPlanMode prints nothing at once, so plan review keeps it. A session without the launch's id and key also prints nothing. While the Inbox is down, the hook re-readsinbox.jsonand retries every 2 s.POST /api/inbox/launch/:id/permissionis guarded like the bridge (loopback Host, no Origin, Bearer token). It then checks the launch key against the launch's settings file (403 wrong_key,410 launch_gone). The route long-polls about 25 s per call and answerswaiting,decidedorwithdrawn.:::questionin the launch thread, from{ claude-code, perm:<session>, "Claude Code" }, with no wake. The person's answer never becomes a second turn, and their own words still go to the real session. The prompt is plain: "Claude Code wants to run a command", the command in a code block, then "in ~/folder". The card's eyebrow reads "Permission" and its Other field "Deny, and tell it why…", through a new optionalquestionChromeseam onQuestionBlock; its default is unchanged. The session choice is never bypass mode and is alwaysdestination: "session".supersedes, so the card reads "No longer applies". Stop and Delete withdraw at once. An Inbox restart adopts open questions.waiting_answer: "Claude Code is waiting on your answer · N min", with Stop. The row goes to Stopped on you, unread. The foot says "1 permission waiting" ("1 waiting" on a phone).Tests
packages/core/inbox-permission.test.ts(12),packages/server/inbox-launch-permission.test.ts(12),apps/hook/server/inbox-permission-hook.test.ts(4), plus additions ininbox-run-line.test.tsandsendTargets.test.ts. Combined run of core, server, hook and repo tests: 4267 pass, 4 fail. The usage-guide failure is fixed. The two certificate-store failures come from this machine. The update-lifecycle failure passes when run alone.tests/e2e/inbox-launch-permission.spec.ts, 7/7, against the compiled Inbox, with the fakeclauderunning the real hook from the settings file. It covers allow once, allow for this session, deny with a reason, answered in Terminal, Stop while waiting, the Inbox stopping and starting mid-wait, and ExitPlanMode. PR 1'sinbox-launch-claude.spec.tsstill passes 10/10.scripts/launch-smoke/permission.ts, with a realclaude --bgforced to--permission-mode default. Allow once rantouch allowed.txt. Deny with a reason blockedtouch denied.txt, and the transcript holds the reason. The host listed the session aswaiting / permission promptthe whole time.Decisions for the owner
inboxLaunch.enabled. This PR keeps it.Finishing for merge
main moved
packages/inbox/reask.tsintopackages/ui/components/inbox-thread/reask.ts, so the filter that keeps a permission prompt out of Re-ask moved there with it.