Skip to content

fix(permission): classify an abbreviated long option like the flag it reaches - #781

Merged
Ishaan Gangwani (ishaan1124) merged 3 commits into
synthetic-sciences:mainfrom
aniruddhaadak80:fix/shell-risk-long-option-abbrev
Sep 29, 2026
Merged

Ishaan Gangwani (ishaan1124) merged 3 commits into
synthetic-sciences:mainfrom
aniruddhaadak80:fix/shell-risk-long-option-abbrev

Conversation

@aniruddhaadak80

Copy link
Copy Markdown
Contributor

What does this PR do, and why?

ShellRisk classifies a bash command so the permission layer can force a
confirmation on the destructive ones. Commands that are not on the MUTATING
blocklist — find, sed, sort, tar, date, file — are checked flag by
flag through one shared matcher:

function has(args: string[], ...values: string[]) {
  return args.some((arg) => values.includes(arg) || values.some((value) => arg.startsWith(`${value}=`)))
}

Exact match, or value=. That misses how GNU tools actually parse options:
getopt_long accepts any unambiguous prefix of a long option. So the
destructive flags are reachable with a different spelling, and the spelling is
what decides whether the user is asked.

I verified each one against real GNU tools (sed 4.9, coreutils sort 8.32,
tar 1.35) rather than reasoning about the parser:

command what it actually did
sed --in-p 's/alpha/ALPHA/' a.txt a.txt became ALPHA — edited in place
sort --outp sorted.txt in.txt wrote sorted.txt with the sorted output
tar --to-c 'echo PWNED-BY-TAR' -xf out.tar printed PWNED-BY-TAR — arbitrary command execution

Each of those classifies as contained on main, and contained is exactly the
level that skips the floor:

// permission/next.ts
if (input.mode === "approve" && input.permission === "bash" && level === "risky") return "ask"

So in a project set to Approve for me with bash allowed, sed --in-p and
tar --to-c run with no confirmation while the commands they are spelled as
always prompt. That is an oversight rather than a design choice — MUTATING
already refuses ed, perl, python, patch, vi and nano outright
precisely because in-place editors are destructive.

The fix teaches has about long-option prefixes:

if (!option.startsWith("--") || !value.startsWith("--")) return false
const prefix = option.slice(2)
return prefix.length > 0 && value.slice(2).startsWith(prefix)

One change covers sed, sort, tar, date and file on both the allow and
the deny side. Over-refusing an abbreviation the tool would have rejected is the
safe direction for an approval floor.

What I did not change, and why

The report I started from also claimed find . -type f -del and -fl reach
-delete and -fls. I tested that and it is false — GNU findutils 4.10
answers find: unknown predicate '-del', and the file survives. Short options
are not abbreviations, so find's short-flag blocklist is not bypassable this way
and is left exactly as it is. There is a test pinning that, so a future change
cannot widen it by accident.

Linked issue

Self-identified. I have not filed a separate report because this turn's issue
budget went to the two apply_patch defects; happy to file one if you would
rather have it tracked.

How did you verify it?

Three cases in backend/cli/test/permission/next.test.ts:

  • an abbreviated long option still classifies as risky — sed --in-p,
    sort --outp, tar --to-c; all three are contained on 3e94875c
  • short options are not abbreviations and stay unaffected — find -del stays
    contained, which is correct
  • the read-only spellings stay contained — find -name, sed -n, plain
    sort, so the fix is not "refuse more"

Commands run:

  • bun test --timeout 120000 ./test/permission/next.test.ts → 80 pass, 0 fail
  • bun run --cwd backend/cli typecheck → exit 0

The whole file matters more than the new cases: making a shared matcher stricter
could easily have flipped one of the 77 existing contained assertions, and none
moved.

The GNU behaviour above was checked with the Git-Bash toolchain on this machine
(C:\Program Files\Git\bin\bash.exe), not assumed.

Checklist

  • bun run check is green (format, typecheck, backend + frontend/ui + SDK tests) — blocked on this Windows checkout by the CRLF and symlink artifacts in my other pull requests; backend typecheck is clean and the full permission suite is green
  • bun run --cwd frontend/workspace build succeeds if I touched frontend/workspace or frontend/ui — not touched
  • ./tooling/repo/generate.ts was run and the tooling/sdk output committed if I changed backend/cli/src/server — not touched
  • CHANGELOG.md has an Unreleased entry if the change is user-visible
  • The matching docs page under frontend/docs/src/content/openscience/ is updated if behavior changed — no doc change needed: the risky/contained classification is internal, and the user-visible effect is that an approval prompt appears where one was being skipped
  • Screenshots or a short video are attached for UI changes — not a UI change
  • No version bumps (package.json versions and tags are written by the release workflow)
  • install and frontend/landing/public/install are still byte-identical if I touched either — not touched

@vercel

vercel Bot commented Sep 28, 2026

Copy link
Copy Markdown

ANIRUDDHA ADAK (@aniruddhaadak80) is attempting to deploy a commit to the InkVell Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks. sed --in-p and sort --outp do reach --in-place and --output, so the bug is real. Three problems with the change in backend/cli/src/permission/shell-risk.ts (lines 220–236):

  1. Regression: the new has lowercases the argument but not the expected value, and the tsc check passes the camelCase "--noEmit". So tsc --noEmit, our most common typecheck, now classifies as risky and prompts in Approve mode. No existing test covers it.
  2. Incomplete: the = form still gets through: sed --in-p=.bak, sort --outp=x and git diff --out=x are all still classified as contained.
  3. Side effects: the same helper feeds checks where a match makes a command allowed (--check, --noEmit, --list), so abbreviations widen those too; unzip --l becomes contained. It also adds false positives: git diff --text matches --textconv, tar --checkpoint matches --checkpoint-action, eslint --f matches --fix.

Suggested shape: no lowercasing; compare only the option name before =:

const name = arg.split("=", 1)[0]
const reaches = name.length > 2 && name.startsWith("--") && value.startsWith(name)

Use that prefix match only in the checks where a match makes a command risky, and keep exact matching for the allow checks. Please add tests pinning tsc --noEmit as contained and sed --in-p=.bak as risky. The tar case should use -tf: with -cf it's already risky on main because of -cf, so it doesn't exercise --to-c.

… reaches

GNU tools accept any unambiguous prefix of a long option, so `sed --in-p` edits
in place, `sort --outp` writes its output file and `tar --to-c 'cmd'` runs cmd
per extracted file. The shared flag matcher only recognised exact spellings, so
those classified as contained and skipped the confirmation a destructive command
always gets. Match a long-option prefix too. Short options are not
abbreviations, so find's blocklist is unchanged.
…level

GNU tools accept any unambiguous prefix of a long option, so `sed --in-p`
edits in place and `sort --outp` writes its output file while spelling the
flag differently. Comparing only the option name before `=` closes the
`--in-p=.bak` form as well, and the match is no longer case-folded, which
had made `tsc --noEmit` read as risky and prompt in Approve mode. The prefix
rule is now confined to the checks where a hit makes a command risky and
stays out of the ones where a hit makes it allowed, so `unzip --l` no
longer widens into a listing and `cargo fmt --check`, `prettier --check` and
`tar -t` are unaffected. Short options and exact long options keep the
plain match, so `sort -o` and `date -s` are unchanged.
@aniruddhaadak80

Copy link
Copy Markdown
Contributor Author

Done in be05b53.

  • Split the helper. has is back to exact matching including name=value, and a new reaches adds the prefix rule. reaches is used at the nine sites where a hit makes a command risky (git --output/--textconv, eslint --fix, biome --write/--fix/--unsafe, sed --in-place, sort -o/--output, tar --checkpoint-action/--to-command/--use-compress-program, date -s/--set, file --compile); the allow checks keep has (prettier --check, tsc --noEmit, cargo fmt --check, unzip -l/--list, tar -t/--list).
  • No case folding, and the comparison is on arg.split("=", 1)[0], so sed --in-p=.bak, sort --outp=x and git diff --out=x are all risky now.
  • One thing beyond your snippet: reaches also runs the plain has match, because the prefix rule alone is gated on startsWith("--") and would have dropped the short options listed at those sites — bare sort -o and date -s would have gone from risky to contained. Short options and exact long options keep the old behaviour; only a long option gets widened.
  • Tests: tsc --noEmit pinned contained, sed --in-p=.bak pinned risky, plus sort --outp=sorted.txt, git diff --out=x and a new case asserting unzip -l is contained while unzip --l is risky, so the prefix rule cannot widen the allowed set again. The tar case now uses -tf, so it is risky because of --to-c and not because of -cf.
  • The git diff --text, tar --checkpoint and eslint --f false positives remain, as in your suggested shape — they are only real options that resolve to themselves, and refusing them is the direction I read as intended. Say the word if you would rather they resolve.
  • Rebased onto f1bdf252.

@aniruddhaadak80

Copy link
Copy Markdown
Contributor Author

All checks are green on be05b537 — the rebase resolved the conflict and the new tests pass. Ishaan Gangwani (@ishaan1124) ready for another look whenever you have a moment.

@ishaan1124
Ishaan Gangwani (ishaan1124) dismissed their stale review September 29, 2026 15:33

The requested changes are in and verified; CI is green.

@ishaan1124
Ishaan Gangwani (ishaan1124) merged commit 1107568 into synthetic-sciences:main Sep 29, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants