Skip to content

feat: directory-scoped shell command approval patterns#896

Open
Aaronontheweb wants to merge 9 commits intonetclaw-dev:devfrom
Aaronontheweb:claude-wt-session-tool-approvals
Open

feat: directory-scoped shell command approval patterns#896
Aaronontheweb wants to merge 9 commits intonetclaw-dev:devfrom
Aaronontheweb:claude-wt-session-tool-approvals

Conversation

@Aaronontheweb
Copy link
Copy Markdown
Collaborator

Summary

  • Directory-scoped approvals for shell commands targeting path-aware verbs (cat, grep, ls, find, etc.). When a user selects "Approve for this chat" or "Approve always", the system stores a directory-scoped pattern (e.g., grep /home/.netclaw/logs/) instead of a per-file pattern. This dramatically reduces approval fatigue — in session D0AC6CKBK5K/1778085593.830269, 11 separate approvals would now be covered by ~2 directory-scoped approvals.
  • Dynamic approval labels show directory scope: "Approve in /home/.netclaw/logs/ for this chat" instead of generic "Approve for this chat".
  • Security posture unchanged: hard deny list, ToolPathPolicy protected paths, and symlink resolution all run after the approval gate. Minimum depth enforcement (2 segments) prevents overly broad scopes like /, /etc/, /home/.

Changes

Area Files What
Pattern extraction ShellTokenizer.cs ExtractDirectoryScope() — scans all args for path tokens, extracts parent directory with min-depth enforcement
Pattern matching ApprovalPatternMatching.cs MatchesDirectoryScope() — trailing / triggers PathUtility.IsWithinRoot() containment check, verb-isolated
Matcher interface IToolApprovalMatcher.cs ExtractDirectoryPatterns() on interface + ShellApprovalMatcher implementation with TraverseSegments helper
Protocol SessionOutput.cs DirectoryPatterns field on ToolInteractionRequest
Approval gate ToolAccessPolicy.cs Computes directory patterns, customizes B/C labels, non-nullable DirectoryPatterns on ToolApprovalContext
Session recording LlmSessionActor.cs Records directory patterns (when available) instead of exact patterns for B/C decisions
Pipeline wiring SessionToolExecutionPipeline.cs, DispatchingToolExecutor.cs Propagates DirectoryPatterns through approval flow

Backward compatibility

  • Storage format (tool-approvals.json) unchanged — directory patterns are strings with trailing / in the same lists
  • Existing exact patterns continue to match as before
  • No migration needed

Test plan

  • ShellTokenizerTestsExtractDirectoryScope for path commands, grep (path in 2nd arg), globs, shallow paths, non-path commands
  • ShellApprovalMatcherTestsExtractDirectoryPatterns for simple paths, compound commands, verb-chain fallback, empty, mixed compound
  • ApprovalPatternMatching — directory-prefix matching via [InlineData] theories (same dir, nested, sibling, verb mismatch, boundary safety)
  • ToolApprovalGateTests — DirectoryPatterns population, directory-scoped labels, default labels for non-path commands
  • dotnet slopwatch analyze — 0 violations
  • Copyright headers verified

Per-file approval patterns for path-aware verbs (ls, cat, grep, find, etc.)
forced users to approve every individual file access separately — 11+ prompts
in a single diagnostic session. Replace with directory-scoped patterns that
cover all files under a parent directory when the user selects "Approve for
this chat" or "Approve always".

Key changes:
- ShellTokenizer.ExtractDirectoryScope() scans all args for file paths (not
  just first positional), extracts parent directory with minimum depth of 2
  segments to prevent overly broad scopes like / or /etc/
- ApprovalPatternMatching gains directory-prefix matching using
  PathUtility.IsWithinRoot() for boundary-safe containment
- IToolApprovalMatcher extended with ExtractDirectoryPatterns(); implemented
  on ShellApprovalMatcher (with compound command + bash -c recursion),
  DefaultApprovalMatcher, and FilePathApprovalMatcher
- DirectoryPatterns wired through ToolInteractionRequest → PendingToolInteraction
  → RecordApprovalAsync so B/C decisions store directory patterns
- Approval option labels dynamically show directory scope (e.g., "Approve in
  /home/.netclaw/logs/ for this chat")

Security: only relaxes the interactive approval gate. Hard deny list,
ToolPathPolicy (protected paths), symlink resolution, and path traversal
prevention layers remain unaffected.
…l code

Add 8 new tests covering directory-scoped approval patterns: 3 in
ToolApprovalGateTests (DirectoryPatterns population, directory-scoped
labels, default labels for non-path commands) and 5 in
ShellApprovalMatcherTests (ExtractDirectoryPatterns for simple paths,
compound commands, verb-chain fallback, empty commands, mixed compounds).

Fix label bug where verb-chain fallbacks like "git push" triggered
directory-scope label formatting, producing nonsense labels like
"Approve in push for this chat". Now checks for trailing '/' to
identify actual directory-scoped patterns before customizing labels.

Simplify code: unify CollectPatterns/CollectDirectoryPatterns into
shared TraverseSegments helper with Func<string, string?> leaf
extractor; narrow bare catch to ArgumentException|IOException; use
PathUtility.ExpandAndNormalize instead of separate ExpandHome +
TryNormalize calls; make DirectoryPatterns non-nullable on
ToolApprovalContext.

Include OpenSpec change artifacts (proposal, design, delta spec, tasks).
@Aaronontheweb Aaronontheweb added security Security-related changes sessions LLM session actor, turn lifecycle, pipelines labels May 6, 2026
Copy link
Copy Markdown
Collaborator Author

@Aaronontheweb Aaronontheweb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found some issues with the tests that make me think the rest of the code might need some belt and suspenders tuning

new ToolName("shell_execute"),
Args("git push origin main"));
Assert.Single(patterns);
Assert.Equal("git push", patterns[0]);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No directories to extract

Args("cat /home/user/.netclaw/logs/crash.log && grep 'error' /home/user/.netclaw/logs/app.log"));
Assert.Equal(2, patterns.Count);
Assert.Contains(patterns, p => p.StartsWith("cat "));
Assert.Contains(patterns, p => p.StartsWith("grep "));
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test needs to be hardened to include the actual directory we're supposed to use for both of these commands. BTW that's not normally how someone would write a command - it'd be piped usually.

@Aaronontheweb
Copy link
Copy Markdown
Collaborator Author

Goal of this PR is to prevent LLM fatigue, having to constantly approve new tool calls all the time, without obliterating our security perimeter around shell tool calls too.

@Aaronontheweb
Copy link
Copy Markdown
Collaborator Author

Tracked Windows-native shell support follow-up in #899.

Use resolved path operands for exact and directory approvals so grep, relative paths, and existing directory targets approve the intended scope. Keep prompt labels, DTO transport, and OpenSpec guidance aligned with the actual approval patterns users are granting.
Keep exact path-aware approval patterns for direct path operands while limiting directory-scoped grants to a small read/list allowlist. Fall back to exact patterns and generic prompts for non-allowlisted commands and redirected shell segments so broader approvals stay predictable and easier to reason about.
Preserve all directly identifiable path operands in exact shell approval patterns and keep those patterns exact-only instead of prefix-expanding to additional operands. Limit directory-scoped grants to single-path allowlisted reads while falling back to exact patterns and generic prompts for multi-path and redirected command shapes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

security Security-related changes sessions LLM session actor, turn lifecycle, pipelines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant