Repository navigation
Description
Summary
The shell search policy misclassifies inline interpreter programs that perform no text search at all — including a pure file write — and blocks the entire atomic shell command.
Scope
Blocking grep, rg, find, fd and the other source/file search commands in favour of sift-light is intended behavior and is not in question here. This report is only about the inline-interpreter classifier, which rejects commands that perform no search at all.
Environment
- sift-light
1.1.2-1 - dsh
0.2.1-rc.3 - macOS 27.0.1 (arm64)
Reproduction A — a write-only inline script is blocked
python3 -c "open('/tmp/sift_policy_probe.txt','w').write(chr(10).join('line %d' % i for i in range(3)))"This reads nothing and searches nothing. It opens a file in write mode and writes three lines.
Actual result:
Error: sift-light search policy blocked direct content search at bash subcommand #1
(python3 inline program …, bytes 0-104); the atomic shell call did not run. Split out
non-search operations, then an equivalent request was not generated because the command
is not one standalone static rg search using the supported option and single-target
subset; manually translate the search, then retry exactly once through sift-light
(possibly MCP-prefixed) with {"pattern":"<search text>","path":"<scope>","scope":"strict"}.
Reproduction B — querying structured data
A routine data query over a JSON file, with no text search intended:
python3 -c "import json; d=json.load(open('/tmp/items.json')); print([r['name'] for r in d if r['kind']=='y'])"Also blocked, classified as a content search.
Root cause
src/search-policy-inline.ts → classifyInlineSearch (bundled into plugins/sift-light/hooks/search-policy.mjs, lines 3638–3659):
const readsFile = /\b(?:open|\w+\.read_text|\w+\.read_bytes)\s*\(/u.test(program);
const searches = /\b(?:re\.(?:search|match|findall|finditer)|\w+\.(?:find|index|count))\s*\(|\bin\s+\w/u.test(program);
return readsFile && searches ? "content" : undefined;Two over-broad predicates combine:
readsFilematchesopen(without inspecting the mode argument.open(path, "w")is a write, not a read, but it satisfies the predicate.- The
\bin\s+\walternative matches ordinary iteration and membership.for i in range(3),for row in rows,"a" in data,if key in mapping— these are language primitives, not searches.
Together, open(...) plus any loop is enough to be classified as a content search. That covers a large fraction of all inline Python that touches a file at all.
The Node/bun branch has the same shape of problem at a smaller scale: readFile( + \.includes( / \.test( treats reading JSON and checking a field as a content search.
Secondary problems
- Collateral loss inside a compound command. When a command really does contain a search that is meant to be redirected (
find,grep), the block is expected. The observation is narrower: a rejected call also discards unrelated subcommands that contain no search at all, sols -la <dir> | head -30never runs if a laterfind <dir> -maxdepth 2 -type f | head -40shares the same atomic call. Splitting the command is the existing workaround, but the caller has to guess which half triggered the block. - Blocking with no legal alternative. When no equivalent sift-light request can be generated, the message still says to "manually translate the search" — but a JSON field query, an aggregation, or a file write has no sift-light equivalent. The agent is left with no permitted path and must invent a different command, which is exactly where unintended workarounds come from.
- Scope of "content search". Right now it means "opens a file and branches on its contents", which is not the same as "searches text for a pattern".
Suggested directions
- Distinguish read from write: require a read-only open (
"r","rb", or a mode-lessopen) before treating a program as a reader. - Drop or constrain the bare
\bin\s+\walternative. If membership testing inside a text blob is the intended signal, it needs a text-shaped target, not anyin. - Require evidence of an actual pattern (a literal or regex plus a text source) rather than "file access plus control flow".
- Prefer a non-blocking advisory when no equivalent sift-light request exists, so legitimate work survives and only the search portion is redirected.
- For compound commands, block or redirect the offending subcommand rather than discarding the entire atomic call.
Note
Reproduction A was executed once, deliberately, as a probe while writing this report. It was not retried, and it was not routed through another script.
Activity
lightsifter commentedon Nov 2, 2026
Thanks for the detailed report and the clear reproductions. You were right: the inline classifier was treating ordinary iteration together with file access as a text search, so even the write-only example could get blocked.
This is fixed in 1.1.2-2 (merged in #126). The classifier now looks for a connection between a file read and a text-matching operation, instead of treating any
open()plusinas a search. I also added coverage for both of your Python examples, JSON field checks in Node, and recognized searches that should still be blocked.One clarification: this doesn't change the atomic behavior for compound shell commands. If a command contains a recognized search, the whole call is still blocked; splitting unrelated operations into separate calls remains the workaround. The inline checks are still heuristic, not a sandbox.
The automated tests passed, including execution of the two reported Python cases through the DSH guard. The updated adapter targets DSH 0.2.1-rc.5; I haven't verified this release in rc.3. Thanks again for taking the time to trace the cause and spell out the distinction.