Approval prompt shows phantom "N directories" scopes for commands with ? in echo text or bare numeric args
Found in: 0.25.3+ (introduced by #1768, "Check every parsed shell path approval scope")
Expected: The approval header names the real directory scopes of the command, or falls back to cwd.
Actual: The header says "Approve in 2 directories?" where neither directory exists on disk — they are artifacts of argument classification, not real paths.
Repro
Run shell_execute with this command (working directory /home/user/repos/demo):
echo "=== stats JSON ==="; curl -s -m 15 "https://example.com/stats" | grep -oE "SearXNG|error|Errors" | head; echo "---try /stats?format=json---"; curl -s -m 15 "https://example.com/stats?format=json" | head -c 2000
The approval prompt renders:
Approve in 2 directories?
• echo
• curl
• grep
• head
ExtractCandidates produces 6 candidates with 2 distinct directories — neither exists:
verb=echo dir=[/home/user/repos/demo/---try]
verb=head dir=[/home/user/repos/demo/2000]
Root cause
Both phantom scopes come from ShellApprovalMatcher.ResolveClauseDirectories / IsAuthorizationPathArg in src/Netclaw.Security/IToolApprovalMatcher.cs:
echo "---try /stats?format=json---" — the ? inside the quoted echo text makes the parser classify the token as a Glob path. ResolveGlobCoveringDirectory derives a covering directory from the static prefix before the glob char: /home/user/repos/demo/---try. The ? is URL query syntax, not a glob.
head -c 2000 — the bare value token 2000 contains no slash, so IsAuthorizationPathArg accepts it as a path (!arg.Raw.Contains('/') branch) and it resolves relative to cwd to /home/user/repos/demo/2000.
Minimal one-command repros, both verified against ShellApprovalMatcher:
echo "---try /stats?format=json---" → 1 candidate, dir /home/user/repos/demo/---try
head -c 2000 → 1 candidate, dir /home/user/repos/demo/2000
echo "a?b" (no slash) and echo "---try /stats---" (no glob char) do not trigger it — both glob classification and a slash-free token are required.
Impact
This fails safe: the phantom scopes force an approval prompt instead of auto-approving, so there is no security hole. The problem is operator trust and signal — the prompt claims specific directory scopes that do not exist, and "Always here" grants persist noise scopes. It also inflates the Approve in N directories header (multi-distinct-dir path added in #962) on routine commands, which is why it surfaced in 0.25.3 after #1768 started classifying every parsed path arg.
Not related to the trailing-slash glob scoping in #1785, though both touch the same matcher.
Suggested fix
Tighten scope extraction so URL-query ? and bare value tokens do not become authorization scopes, while keeping #1768's intent of checking every real path arg:
- In
IsAuthorizationPathArg, require a slash or a known path shape (., .., ~, @file) before treating a slash-free token as a path; bare numerics and ---try-style tokens should not qualify.
- In
ResolveGlobCoveringDirectory, do not classify a token as a glob scope when the ? occurs after a / in a URL-shaped token (query string) — or require the glob prefix to resolve to an existing directory before emitting a scope.
- Add regression cases to
ShellApprovalMatcherTests for both minimal repros above.
Approval prompt shows phantom "N directories" scopes for commands with
?in echo text or bare numeric argsFound in: 0.25.3+ (introduced by #1768, "Check every parsed shell path approval scope")
Expected: The approval header names the real directory scopes of the command, or falls back to cwd.
Actual: The header says "Approve in 2 directories?" where neither directory exists on disk — they are artifacts of argument classification, not real paths.
Repro
Run
shell_executewith this command (working directory/home/user/repos/demo):The approval prompt renders:
ExtractCandidatesproduces 6 candidates with 2 distinct directories — neither exists:Root cause
Both phantom scopes come from
ShellApprovalMatcher.ResolveClauseDirectories/IsAuthorizationPathArginsrc/Netclaw.Security/IToolApprovalMatcher.cs:echo "---try /stats?format=json---"— the?inside the quoted echo text makes the parser classify the token as aGlobpath.ResolveGlobCoveringDirectoryderives a covering directory from the static prefix before the glob char:/home/user/repos/demo/---try. The?is URL query syntax, not a glob.head -c 2000— the bare value token2000contains no slash, soIsAuthorizationPathArgaccepts it as a path (!arg.Raw.Contains('/')branch) and it resolves relative to cwd to/home/user/repos/demo/2000.Minimal one-command repros, both verified against
ShellApprovalMatcher:echo "---try /stats?format=json---"→ 1 candidate, dir/home/user/repos/demo/---tryhead -c 2000→ 1 candidate, dir/home/user/repos/demo/2000echo "a?b"(no slash) andecho "---try /stats---"(no glob char) do not trigger it — both glob classification and a slash-free token are required.Impact
This fails safe: the phantom scopes force an approval prompt instead of auto-approving, so there is no security hole. The problem is operator trust and signal — the prompt claims specific directory scopes that do not exist, and "Always here" grants persist noise scopes. It also inflates the
Approve in N directoriesheader (multi-distinct-dir path added in #962) on routine commands, which is why it surfaced in 0.25.3 after #1768 started classifying every parsed path arg.Not related to the trailing-slash glob scoping in #1785, though both touch the same matcher.
Suggested fix
Tighten scope extraction so URL-query
?and bare value tokens do not become authorization scopes, while keeping #1768's intent of checking every real path arg:IsAuthorizationPathArg, require a slash or a known path shape (.,..,~,@file) before treating a slash-free token as a path; bare numerics and---try-style tokens should not qualify.ResolveGlobCoveringDirectory, do not classify a token as a glob scope when the?occurs after a/in a URL-shaped token (query string) — or require the glob prefix to resolve to an existing directory before emitting a scope.ShellApprovalMatcherTestsfor both minimal repros above.