12 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 45 | Add "approve for this session" option to permission prompts |
Session-scoped approvals for permission prompts
Problem Statement
When the external_directory gate is set to ask, every file access outside CWD prompts the user individually.
Investigating a sibling project can trigger dozens of identical approval dialogs in a single session.
There is no way to say "yes, allow this class of access for the rest of the session" without changing the on-disk policy to allow.
Goals
- Add a third dialog option ("Allow for session") alongside "Yes" and "No" in the permission confirmation UI.
- Introduce an in-memory
SessionApprovalCachethat records directory-prefix approvals. - Before prompting, check the cache; if a matching session approval exists, skip the dialog and log
resolution: "session_approved". - Scope session approvals to the external-directory surface only (both file-tool and bash variants).
- Clear the cache on
session_shutdown. - Do not persist approvals to disk — they are ephemeral by design.
- Record session-approved decisions in the review log with a distinct resolution value.
Non-Goals
- Extending session approvals to tool/bash-pattern/MCP/skill surfaces (future work noted in the issue).
- Per-agent scoping of session approvals (use the same flat cache regardless of active agent).
- Persisting approvals across sessions — that is what policy config is for.
- Changing the on-disk schema, example config, or
defaultPolicyvalues. - Changing the
/permission-systemslash command.
Background
Permission surfaces involved
special.external_directory — evaluated before normal tool/bash checks for path-bearing file tools and bash commands referencing external paths.
Existing modules
| Module | Role |
|---|---|
src/permission-dialog.ts |
requestPermissionDecisionFromUi() presents Yes/No/No-with-reason via PermissionDecisionUi.select(). |
src/permission-gate.ts |
applyPermissionGate() — pure deny/ask/allow branching. Receives a promptForApproval callback. |
src/external-directory.ts |
Path normalization, outside-CWD detection, message formatting. |
src/index.ts |
Wires the gate for file-tool and bash external-directory checks. Calls promptPermission() which delegates to requestPermissionDecisionFromUi(). |
src/types.ts |
PermissionPromptDecision, PermissionDecisionState types. |
Flow today
tool_callhandler detects external path.- Calls
applyPermissionGate({ state: extCheck.state, ... }). - If
state === "ask", gate callspromptForApproval()→promptPermission()→requestPermissionDecisionFromUi(). - User sees Yes / No / No-with-reason.
- Decision is logged and returned.
Flow after this change
tool_callhandler detects external path.- New: before calling the gate, check
SessionApprovalCachefor a matching directory prefix. If found → logsession_approved, return{ action: "allow" }without prompting. - If not cached → call
applyPermissionGate()as today, but with the new three-option dialog. - If user selects "Allow for session" → record the directory prefix in the cache, return approved.
session_shutdownclears the cache.
Design Overview
SessionApprovalCache
A small class with a Map<string, Set<string>> keyed by surface type (initially only "external_directory").
Values are normalized directory prefixes.
export class SessionApprovalCache {
private approvals = new Map<string, Set<string>>();
approve(surface: string, prefix: string): void;
has(surface: string, path: string): boolean; // prefix match
clear(): void;
}
has() checks whether any stored prefix for the surface is an ancestor of (or equal to) the given path using isPathWithinDirectory() from external-directory.ts.
Extended dialog
PermissionDecisionState gains a fourth value: "approved_for_session".
PermissionPromptDecision already carries state; callers inspect it to decide whether to cache.
The dialog options become:
Yes | Yes, for this session | No | No, provide reason
requestPermissionDecisionFromUi() returns { approved: true, state: "approved_for_session" } for the session option.
Deriving the approval prefix
For file-tool external-directory checks, the prefix is the parent directory of the target path (so approving access to ~/other-project/src/foo.ts covers ~/other-project/src/).
For bash external-directory checks, each extracted external path's parent directory is recorded.
A helper deriveApprovalPrefix(normalizedPath: string): string returns dirname(normalizedPath) with a trailing separator, ensuring prefix matching works correctly.
Review log
When a request is satisfied from the cache:
{
"event": "permission_request.session_approved",
"resolution": "session_approved",
"sessionApprovalPrefix": "/Users/.../other-project/src/"
// ... standard log context
}
Integration in src/index.ts
- Instantiate
SessionApprovalCachealongsidepermissionManagerat the top ofpiPermissionSystemExtension(). - Clear it in
session_shutdown. - In both external-directory gate sites (file-tool and bash), insert a cache check before
applyPermissionGate(). - After a successful prompt where
decision.state === "approved_for_session", callcache.approve(...).
Module-Level Changes
New file: src/session-approval-cache.ts
SessionApprovalCacheclass.deriveApprovalPrefix(normalizedPath: string): stringhelper.- Exports only pure logic; no IO.
Modified: src/permission-dialog.ts
- Add
"approved_for_session"toPermissionDecisionState. - Add a fourth option constant
APPROVE_FOR_SESSION_OPTION = "Yes, for this session". - Update
PERMISSION_DECISION_OPTIONSarray. - Handle the new option in
requestPermissionDecisionFromUi(). - Update
isPermissionDecisionState()guard.
Modified: src/types.ts
No changes needed — PermissionDecisionState lives in permission-dialog.ts.
Modified: src/index.ts
- Import
SessionApprovalCacheandderiveApprovalPrefix. - Instantiate cache in
piPermissionSystemExtension(). - Clear cache in
session_shutdownhandler. - File-tool external-directory block: add cache-check before gate, cache-write after session-approved decision.
- Bash external-directory block: same pattern, iterating over each extracted external path.
New file: tests/session-approval-cache.test.ts
- Unit tests for
SessionApprovalCache(approve, has, prefix matching, clear, cross-surface isolation).
Modified: tests/permission-dialog.test.ts
- Test the new "Yes, for this session" option returns
approved_for_session. - Test
isPermissionDecisionStateincludes the new value.
Modified: tests/index.test.ts (or integration-level test)
- Test that a session-approved external-directory decision skips subsequent prompts for paths under the same prefix.
- Test that
session_shutdownclears session approvals. - Test that session approvals do not leak across surfaces.
TDD Order
-
Red → Green:
SessionApprovalCache— approve, has (prefix match), clear, surface isolation.test: cover SessionApprovalCache approve/has/clear -
Red → Green:
deriveApprovalPrefix— returns parent dir with trailing separator, handles root paths.test: cover deriveApprovalPrefix edge cases -
Feat: implement
SessionApprovalCacheandderiveApprovalPrefixinsrc/session-approval-cache.ts.feat: add SessionApprovalCache for ephemeral session approvals -
Red → Green: permission dialog returns
approved_for_sessionfor session option.test: cover "Yes, for this session" dialog option -
Feat: extend
requestPermissionDecisionFromUi()with the session option.feat: add "approve for session" option to permission dialog -
Red → Green: integration — file-tool external-directory cache check skips prompt; bash variant likewise; shutdown clears cache.
test: cover session-approved external-directory flow -
Feat: wire
SessionApprovalCacheintosrc/index.tsexternal-directory gates.feat: wire session approvals into external-directory gates -
Docs: update README permission-dialog section if it documents the Yes/No options.
docs: document session-scoped approval option
Risks and Mitigations
| Risk | Mitigation |
|---|---|
| Session approval silently weakens a permission by covering more paths than intended. | Prefix is derived from dirname() of the specific path, not the top-level external directory. Approving ~/other/src/foo.ts covers ~/other/src/ but not ~/other/. Users must approve broader prefixes explicitly via repeated prompts or policy config. |
approved_for_session state breaks callers that only expect three states. |
isPermissionDecisionState() is updated in the same commit. Only src/index.ts inspects decision.state for caching; all other callers check decision.approved (boolean). |
| Cache grows without bound during long sessions. | External-directory prefixes are short strings; even hundreds of approvals are negligible. No eviction needed. |
| Bash external-directory extracts multiple paths — unclear which to cache. | Cache each extracted path's parent individually. This is consistent: each path that was flagged gets its prefix recorded. |
| Yolo mode interaction — session approval is redundant when yolo auto-approves. | No conflict: yolo mode short-circuits before the dialog is shown, so the cache is never consulted. No special handling needed. |
Open Questions
- Should the dialog show the resolved prefix being approved (e.g., "Allow all access to ~/other-project/src/ for this session")? Leaning yes for transparency, but can be deferred to a follow-up polish pass.
- Should the session approval cover the exact directory of the path or its parent?
Current design uses
dirname()(parent of the file). If the user is accessing~/other-project/README.md, the prefix is~/other-project/which seems right. For directory-bearing tools likefindandlswhere the path is a directory, using the path itself as the prefix may be more appropriate — worth validating in tests.