Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
aa5a505
feat: directory-scoped shell command approval patterns
Aaronontheweb May 6, 2026
b16e4b7
feat: add tests, fix label bug, and simplify directory-scoped approva…
Aaronontheweb May 6, 2026
cac32ec
Merge branch 'dev' into claude-wt-session-tool-approvals
Aaronontheweb May 6, 2026
54f77b8
fix shell approval path resolution and prompt scoping
Aaronontheweb May 6, 2026
d40180f
fix(security): narrow directory-scoped shell approvals
Aaronontheweb May 6, 2026
1d3b202
fix(security): harden shell approval multi-path matching
Aaronontheweb May 7, 2026
ed091a1
Revert "fix(security): harden shell approval multi-path matching"
Aaronontheweb May 7, 2026
47b09d8
Revert "fix(security): narrow directory-scoped shell approvals"
Aaronontheweb May 7, 2026
1f28599
Revert "fix shell approval path resolution and prompt scoping"
Aaronontheweb May 7, 2026
13aeaa3
fix(security): reuse shell approvals by directory root
Aaronontheweb May 7, 2026
9245aee
fix(tests): update subagent approval expectation
Aaronontheweb May 7, 2026
8f1c0e9
fix(security): preserve shell approval roots across session flows
Aaronontheweb May 7, 2026
fb3056a
fix(security): preserve shell path semantics in approvals
Aaronontheweb May 7, 2026
9827c0a
fix(security): preserve shell separators in display roots
Aaronontheweb May 7, 2026
e740419
feat(security): split shell approvals by shell family
Aaronontheweb May 7, 2026
b67d3dd
fix(security): choose shell approvals by command semantics
Aaronontheweb May 7, 2026
a878024
Merge branch 'dev' into claude-wt-session-tool-approvals
Aaronontheweb May 7, 2026
9a9f787
fix(security): keep POSIX approval paths host-agnostic
Aaronontheweb May 7, 2026
739d453
fix(security): preserve POSIX roots on Windows hosts
Aaronontheweb May 7, 2026
aba83e6
fix(windows): stabilize shell approvals and Docker-backed tests
Aaronontheweb May 7, 2026
0a6072a
fix(windows): preserve native separators in shell approval comparison…
Aaronontheweb May 7, 2026
0863411
Merge branch 'dev' into claude-wt-session-tool-approvals
Aaronontheweb May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-06
122 changes: 122 additions & 0 deletions openspec/changes/directory-scoped-approval-patterns/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
## Context

The current shell approval flow reuses exact command patterns too narrowly for
diagnostic work. In session `D0AC6CKBK5K/1778163885.517639`, repeated shell work
inside the same directories still produced repeated prompts because each new
file path became a new approval target. Issue `#905` is only contextual here:
it explains why that one session had so many shell calls, but this change does
not attempt to solve the broader issue volume.

This update shifts broader shell approvals away from command classification and
toward reusable local directory roots. The approval question becomes: can this
shell approval unit be shown to stay under roots the user already approved?

When the answer is yes, later shell commands under those roots should not ask
again. When the answer is no, the system must fall back to exact approval
behavior.

The approval system has three security layers:
1. Hard deny list (before approval gate)
2. Interactive approval gate (`ToolAccessPolicy` + `IToolApprovalService`)
3. `ToolPathPolicy` protected-path enforcement (at execution time, after approval)

This change only relaxes layer 2. Layers 1 and 3 are unaffected.

## Goals / Non-Goals

**Goals:**
- Reduce repeated shell approval fatigue for later commands under the same local
directories
- Keep `Approve once` exact blocked-call retry only
- Store reusable directory roots for shell B/C approvals when local roots are
extractable
- Auto-approve later shell approval units only when all recognized local paths
stay under already approved roots
- Keep boundary-safe root matching and minimum-depth enforcement
- Show root context in approval option labels

**Non-Goals:**
- Changing the hard deny list or `ToolPathPolicy` behavior
- Changing `Approve once` (A) behavior
- Classifying shell safety by command verb families
- Using directory-root approvals when no reusable local roots can be extracted
- Glob-aware or regex-based approval matching

## Decisions

### Approval units: split on shell control operators, not pipelines

The broader approval model operates on shell approval units instead of whole
commands or individual tokens.

- `&&`, `||`, and `;` start a new approval unit
- `|` stays inside the current approval unit

This preserves the user's expectation that a pipeline like
`grep ... /home/.netclaw/logs/app.log | wc -l` is one piece of work, while still
preventing a later `&& rm ...` segment from inheriting that approval.

### Directory roots replace verb-scoped directory patterns

For `shell_execute`, B and C approvals store reusable local directory roots, not
verb-specific patterns. A later `ls`, `cat`, or `grep` can reuse the same root
approval as long as every recognized local filesystem path in that approval unit
resolves under approved roots.

This is intentionally verb-agnostic. The safety boundary moves from shell verb
classification to filesystem containment plus the existing backstops.

### Extraction: recognized local filesystem paths across the whole unit

Root extraction scans each approval unit for recognized local filesystem paths,
not just the first positional token. That covers forms like
`grep -l "timeout" /home/.netclaw/logs/daemon.log`, multiple path arguments, and
paths inside a pipeline.

If one or more local paths are found, the system derives directory roots from
them. If none are found, directory-root approval is unavailable and the system
falls back to exact approval behavior for that unit.

### Matching: all recognized local paths must stay under approved roots

Auto-approval succeeds only when every recognized local filesystem path in the
candidate approval unit resolves under an already approved root.

This avoids partial matches where one safe path could accidentally approve a
unit that also touches another directory the user never approved.

### Root comparison remains boundary-safe

Root matching delegates to `PathUtility.IsWithinRoot()`, which normalizes paths,
applies platform-appropriate case sensitivity, and checks boundaries. This keeps
`/home/usersecret` from matching a root approval for `/home/user`.

### Minimum depth: 2 segments below root

Derived roots shallower than 2 segments are rejected. That still blocks broad
roots such as `/`, `/etc/`, and `/tmp/` from becoming reusable directory
approvals. Those commands can still proceed through exact approval behavior.

### Tiny internal representation: display path vs comparison root

Internally, each extracted directory root should carry a tiny pair:

- a display path for approval labels
- a normalized comparison root used for containment checks

This keeps the behavior model focused on roots while avoiding UI drift from the
path form used for comparisons.

## Risks / Trade-offs

**[Risk] Root approvals are broader than per-file approvals** → Mitigated by
minimum depth enforcement, normalization, traversal checks, and
`ToolPathPolicy.CommandReferencesDeniedPath()` at execution time.

**[Risk] Multi-path approval units can be harder to explain in the prompt** →
The prompt can display the primary extracted roots, but matching still requires
all recognized local paths in the unit to stay under approved roots.

**[Risk] Some shell commands have no reusable local roots** → This is expected.
When no local roots are extractable, the system falls back to exact approval
behavior instead of inventing a broader approval class.
57 changes: 57 additions & 0 deletions openspec/changes/directory-scoped-approval-patterns/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
## Why

In session `D0AC6CKBK5K/1778163885.517639`, repeated shell investigation under
the same working directories created approval fatigue because each new file path
produced another approval prompt. Issue `#905` helps explain why that session
had unusually high shell call volume, but the scope of this change is narrower:
reduce repeated approvals for later shell commands that stay within already
approved local directory roots.

The exact-match restriction is still necessary when the system cannot identify a
safe reusable local root. What needs to change is the granularity of broader
shell approvals, not the safety backstops.

## What Changes

- `Approve once` remains exact blocked-call retry only.
- For `shell_execute`, `Approve for this chat` (B) and `Approve always` (C)
store **directory roots** instead of verb-specific or command-pattern-specific
approvals when the approval unit contains recognizable local filesystem paths.
- Directory approvals are root-based and verb-agnostic: later shell approval
units are auto-approved when all recognized local filesystem paths in that
unit resolve under already approved roots.
- Shell approval units split on `&&`, `||`, and `;`, but keep pipelines joined
so commands like `grep ... | wc -l` are covered by one directory-root
approval.
- If a shell approval unit yields no local directory roots, broader directory
approval does not apply and the system falls back to exact approval behavior.
- Minimum directory depth, path normalization, path traversal checks, and
`ToolPathPolicy` remain the safety backstop.
- `DirectoryPatterns` is renamed to `DirectoryRoots` throughout this change.

## Capabilities

### New Capabilities

(none)

### Modified Capabilities

- `tool-approval-gates`: Adds directory-root extraction, storage, matching, and
display for shell command approvals. Extends shell approval-unit parsing,
root matching, `IToolApprovalMatcher`, persistent approval storage, and the
`ToolInteractionRequest` protocol.

## Impact

- **Security**: Only relaxes the interactive approval gate. Hard deny rules,
minimum root depth, normalization, traversal checks, and `ToolPathPolicy`
remain unchanged and continue to block protected targets even after a broader
root approval.
- **Code**: `ShellTokenizer`, shell approval-unit traversal, root matching,
`IToolApprovalMatcher` (+ implementations), `ToolAccessPolicy`,
`ToolApprovalContext`, `ToolInteractionRequest`, `PendingToolInteraction`,
`LlmSessionActor`, `SessionToolExecutionPipeline`.
- **Backward compatibility**: Existing exact approvals continue to work
unchanged. `DirectoryRoots` defaults to empty on protocol types when no local
reusable roots are available.
Loading
Loading