Repository navigation
feat(freshclaude): approvals use Claude's own "don't ask again" rules, and plan exits show a plan card - #872
Merged
Merged
Conversation
… (D0) A real-CLI contract over the production sidecar, the pinned SDK 0.3.237 and its CLI 2.1.237 against the local provider: the suggestion shapes for Bash, Write, a Read outside the workspace, a compound command and ls /; that a localSettings rule sent back as updatedPermissions writes .claude/settings.local.json and stops the next ask for that command only; that a session rule does the same with no file; that ExitPlanMode carries the plan file's text and path when the model wrote one; and that a plain, acceptEdits or bypassPermissions plan approval leaves plan mode for that mode, stated once by the sidecar, with the next Write asking or not to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude_permission_update.rs keeps only well-formed updates of the SDK's documented variants (addRules, replaceRules, removeRules, setMode, addDirectories, removeDirectories), rebuilt with exactly their documented keys, so a suggestion relayed into the strict snapshot can never fail the client's parse. validate_decision accepts a decision whose updatedPermissions is absent or a list of such updates, and refuses anything else. The next commits wire both in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pending-approval fold keeps an sdk.permission.request's suggestions, normalized at fold time, and the snapshot overlay emits them as pendingApprovals[].suggestions (absent when none survive). The shared contract gains FreshAgentPermissionUpdateSchema, a strict discriminated union mirroring the server's normalizer, so a well-formed list parses and one malformed suggestion fails the strict snapshot parse, which is why the server normalizes first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d not accept (D4)
handle_approval_respond now checks the decision with validate_decision
before writing it to the sidecar. A malformed updatedPermissions list is
answered with freshAgent.error{code:'INVALID_APPROVAL_DECISION'} and a
warn log (freshagent.claude.approval_decision_refused); nothing reaches
the CLI and the request stays pending, so a retry with well-formed
updates lands verbatim. A new WS test over the real sidecar and the CLI
simulator pins the whole path: the CLI's suggestions reach the snapshot
normalized, a malformed list is refused with the card kept, and the
suggestions sent back reach the CLI exactly, whose accepted setMode then
reaches every client as the session's mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (D5) When the CLI changes its own mode (an approved ExitPlanMode, or a setMode suggestion the user accepted), the stdout consumer already folded it into the session's live settings. It now also writes the session's binding row, through the same adoption a settings apply uses, so a resume after a server restart boots the conversation in the mode the CLI was in rather than the mode the pane created it with. Pinned over the real sidecar and the CLI simulator with a durable ledger and a restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uage suggestion lines (D8) src/lib/fresh-agent-permission-updates.ts builds every approval answer: a plain allow (never updatedInput), an allow that applies exactly the suggestions shown, a deny that keeps the turn going, and the plan card's four choices. describePermissionUpdate says in one line what accepting a suggestion does and where it is kept (this conversation, the project's .claude/settings.local.json or settings.json, or every project), and rememberLabel names the remember button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the claude client-only bypass (D8, D10) The approval card answers through one onRespond(decision) built by the shared helpers. When the CLI suggested permission updates, it offers "Allow and don't ask again" (or "Allow and auto-accept edits" for a lone acceptEdits switch), spells each suggestion out underneath (the button's description), and sends exactly those suggestions as updatedPermissions, so Claude itself stops asking and enforces the scoped rule. The client-only bypass renders only when the caller passes onAlwaysAllow: the view does so for codex and opencode, relabelled "Always allow <Tool> in this pane" with the tooltip "Until this pane reloads" (decision D3), and its auto-answer effect never runs for a claude pane, so no claude request is answered without a click. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ExitPlanMode is now reviewed as a plan instead of a tool call showing raw JSON. FreshAgentPlanApprovalCard (role=alert, "Plan approval request") renders input.plan as markdown, shows planFilePath, says so when no plan text came, and offers the four choices of decision D4: "Approve plan" (a plain allow, so the CLI returns to its pre-plan mode), "Approve and auto-accept edits" and "Approve and stop asking for permission" (a session setMode), and "Keep planning" with an optional note sent as the deny reason. Every choice is a 44 px touch target. The approval card hands ExitPlanMode requests to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sendDeckApproval builds its decision with buildAllowDecision, so every approval answer the client sends comes from one builder. The decision is unchanged: a plain allow with no updatedInput. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ecar (D12) freshclaude-approvals-rust.spec.ts drives the real server, the production sidecar and the CLI simulator: "Allow and don't ask again" sends the CLI's suggestions as updatedPermissions and a later request waits for a click; the plan card renders the plan, and "Approve and auto-accept edits" moves the CLI to acceptEdits, the gear shows Accept edits and the next send runs in it; kilroy gets the same remember button. The claude lane's "Always Allow answers the second raise without a click" is removed from fresh-agent-control-rust: it pinned the client-only bypass this change retires for claude, and the new spec's first case replaces it with the stronger opposite. Coverage of the kept codex bypass moves to a new codex-lane case, "Always allow (pane) auto-answers the second raise for that tool". The live-transcript plan case now approves through the plan card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ClaudeSession::cli_session_id is read by the settings paths (a settings apply, and now the CLI's own permission-mode change) to write the session's binding row, so its doc no longer claims nothing reads it and it drops the stale dead_code allow. The snapshot overlay's doc is re-wrapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ool card Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plan card's "Approve and stop asking for permission" also matches a substring search for "Stop", so waitForIdle hit a strict-mode violation while a plan card was up. The composer's button is named exactly "Stop"; the lane helper and the live-transcript spec now say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plan card's "What should change?" note is sent only with "Keep planning"; an approval carries no message. Clicking an approve button with a note typed dropped the note and ran the plan unchanged, including "Approve and stop asking for permission". While a note is typed the three approve buttons are now disabled and described by a line saying the note goes only with Keep planning; clearing it re-enables them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…by content The `freshagent.claude.approval_decision_refused` warning and the INVALID_APPROVAL_DECISION error frame carried the whole rejected permission update, including rule content such as Bash commands and paths. Claude logs carry ids, numbers and codes only. validate_decision now returns a typed DecisionRefusal (not a list, or the failing update's index and its variant, "unknown" for a type Claude does not define); the log records update_index/update_type and the error frame says "updatedPermissions[1] (addRules) is not a permission update Claude accepts". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kilroy is hidden unless the server sets KILROY_ENABLED and the README does not otherwise mention it, so the sentence about Claude's own "don't ask again" rules and the plan card names Freshclaude only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
What users see
What changed
test/integration/providers/claude-permission-contract.{mjs,test.ts}runs the production sidecar against the local fake endpoint. It pins the CLI's permission suggestions, sending them back in both directions, and plan approvals in every mode.crates/freshell-freshagent/src/claude_permission_update.rs.updatedPermissionsClaude would not accept is refused withINVALID_APPROVAL_DECISION. Nothing is sent to Claude, and the request stays open so the user can answer again.src/lib/fresh-agent-permission-updates.ts, which also writes the plain-language rule lines.FreshAgentPlanApprovalCard.freshclaude-approvals-rust.spec.tsdrives the real sidecar against the CLI simulator. The live-transcript plan case now approves through the plan card.Tests (exact results)
cargo test -p freshell-freshagent --lib: 1510 passed.cargo test -p freshell-ws --test freshagent_claude_approvals --test freshagent_claude_lifecycle --test freshagent_claude_live_rendering --test freshagent_control_reply: 2 + 12 + 2 + 17 passed. After the last fixes,freshagent_claude_approvalswas rerun: 2 passed.cargo clippy -p freshell-freshagent --all-targets -- -D warningsandcargo fmt --all -- --check: both clean.pnpm run test:vitest run test/unit/client/components/fresh-agent test/unit/client/lib/fresh-agent-permission-updates.test.ts test/unit/client/lib/fresh-agent-ws.test.ts test/unit/client/deck test/unit/shared/fresh-agent-contract.test.ts test/unit/claude-sidecar --config config/vitest/vitest.config.ts: 1481 passed.pnpm run test:vitest run test/integration/providers/claude-permission-contract.test.ts --config config/vitest/vitest.config.ts(pinned CLI, local fake endpoint): 9 passed.pnpm run typecheck:client: clean.pnpm run test:e2e test/e2e-browser/specs/freshclaude-approvals-rust.spec.ts test/e2e-browser/specs/fresh-agent-control-rust.spec.ts test/e2e-browser/specs/freshclaude-live-transcript-rust.spec.ts --project=chromium(cloud backend, commit f1c95da): 40 passed.Follow-ups
addPermissionRequestinsrc/store/freshAgentSlice.tsthrows when a permission request arrives for a session the client has no record of.eslintreports 5 errors insrc/components/fresh-agent/FreshAgentView.tsx, none on lines this PR changed.docs/index.html(the static mock) does not show the plan card yet.freshell-session-host'sprovider_config_codex_linked_skills_and_discovery_survive_repeated_resumefails on any machine with a stray/tmp/node_modulesfolder, because its fixture lives under/tmpand node_modules folders above a resource are copied. Someone rannpm install wsin/tmptoday. The local pre-push passed withFRESHELL_TEST_EXECUTABLE_TMP_DIRpointed at/var/tmp, and all 169 tests in that crate passed.🤖 Generated with Claude Code