Skip to content

False positive: write-only inline scripts (and JSON data queries) are blocked as content searches #125

Closed

Description

@sakisakisa-design

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:

  1. readsFile matches open( without inspecting the mode argument. open(path, "w") is a write, not a read, but it satisfies the predicate.
  2. The \bin\s+\w alternative 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, so ls -la <dir> | head -30 never runs if a later find <dir> -maxdepth 2 -type f | head -40 shares 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-less open) before treating a program as a reader.
  • Drop or constrain the bare \bin\s+\w alternative. If membership testing inside a text blob is the intended signal, it needs a text-shaped target, not any in.
  • 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

  1. lightsifter commented on Nov 2, 2026

    @lightsifter
    Owner

    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() plus in as 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions