# Tool approvals **Status:** Implemented · **Code:** `Scripts/permissions.js`, `Scripts/diff.js` Every `can_use_tool` control request becomes a decision surface. The request is registered in the sidebar **first**, then surfaced — so dismissing a notification or panel never strands the session. ## Routing `_present(approval)` picks the surface: | Request | Surface | | --- | --- | | `AskUserQuestion` | [Questions](questions.md) | | `ExitPlanMode` | [Plan mode](plan-mode.md) | | A file change (`preview` reconstructed) | Editable proposal, below | | Other `requires_user_interaction` | Denied with an explanation | | Anything else | Action panel (Allow / Allow for Session / Review / Deny) | ## `PermissionResult` shapes ```jsonc {"behavior":"allow"} {"behavior":"allow","updatedInput":{…}} // run with different input {"behavior":"allow","updatedPermissions":[{…}]} // + change rules or mode {"behavior":"deny","message":"…"} // message is shown to Claude ``` `updatedPermissions` entries used here: `{type:"addRules", rules:[{toolName}], behavior:"allow", destination:"session"}` for *Allow for Session*, and `{type:"setMode", mode, destination:"session"}` for plan approval. ## Editable proposals The claudecode.nvim lesson: a proposal should be a **document you can amend**, not a yes/no gate. When Claude wants to edit or create a file: 1. `diff.proposedChange()` reconstructs the file **as it would be after** the change (applying `old_string`→`new_string`, or the `edits[]` in order). 2. That content is staged to `/proposed//` and opened. 3. Editor gestures settle it: | Gesture | Result | | --- | --- | | Save (`onDidSave`) | Apply — with any edits the user made | | Close (`onDidDestroy`) | Deny | A notification and the sidebar rows carry the same choices. **Why a staged real file, not an untitled document:** an untitled document cannot be saved without a Save dialog, which would break the gesture. Keeping the original file extension also preserves syntax highlighting. Each request stages into its own directory so two files with the same basename can be under review at once while the tab still reads `index.js`. Stale proposals are cleared on new session (`Controller.clearStagedProposals`). **Expressing an amendment.** `_inputForContent()` rewrites the tool input as a whole-file replacement rather than trying to recover a minimal edit: | Tool | Amended input | | --- | --- | | `Write` | `{…input, content}` | | `Edit` / `Update` | `{…input, old_string: , new_string: , replace_all: false}` | | `MultiEdit` | `{…input, edits:[{old_string: , new_string: }]}` | The whole original file is unique within itself by construction, so `old_string` always matches exactly once — and the result is precisely what the user saw. Set `claudenova.reviewEdits` to `panel` for the older quick yes/no. ## Diff rendering `diff.js` is an LCS line diff with common prefix/suffix trimming and a 1,000,000-cell guard beyond which it degrades to a plain replacement. Rendered unified with 3 lines of context, capped at 400 lines. `splitLines` drops the final empty element after a trailing newline — otherwise every diff carried a phantom blank line and a new file showed `-1`. ## Gotchas - `requires_user_interaction: true` does **not** mean "cannot be handled" — it means one-tap approve/deny is wrong because the tool's own card is the interaction surface. Both tools that set it are handled natively; blanket denying it (the first implementation) made plan mode a dead end. - Sidebar `allow`/`deny` must not bypass the plan and question flows — a bare allow on a plan would skip the `setMode` update and leave the session stuck in plan mode. `decide()` routes those two back through `_present()`.