Skip to content

Add a 'deny-write' option - #51

Merged
Pierozi merged 2 commits into
agentic-dev3o:mainfrom
jaredly:deny-write
Sep 18, 2026
Merged

Pierozi merged 2 commits into
agentic-dev3o:mainfrom
jaredly:deny-write

Conversation

@jaredly

@jaredly jaredly commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

In my applicaiton, I want to run commands without granting write access to the working directory, which there didn't seem to be an option to make that work. I thought about doing a --dont-auto-allow-cwd or something like that, but I think --deny-write is more generally applicable, as it also works for when you want to exclude writes to a subset of an explicitly allowed file tree.

In my applicaiton, I want to run commands without granting write access to the working directory, which there didn't seem to be an option to make that work. I thought about doing a `--dont-auto-allow-cwd` or something like that, but I think `--deny-write` is more generally applicable, as it also works for when you want to exclude writes to a subset of an explicitly allowed file tree.
@Pierozi

Pierozi commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Hi @jaredly, thanks a lot for this PR! 🙏 --deny-write is a very welcome addition — it's the natural counterpart to --deny-read, and you wired it through every layer exactly the way the codebase expects (CLI args, config schema, profile composition, merge, shell completions, --init template, README). Really appreciate the thoroughness.

That said, there is one critical issue we need to fix before merging, plus a few smaller gaps.

1. Rule ordering makes the deny ineffective (blocking)

Seatbelt uses last-match-wins semantics: a later allow overrides an earlier deny for overlapping paths. The new deny file-write* block is emitted in generate_seatbelt_profile before both:

  • the working-directory rule (allow file* (subpath <cwd>)), and
  • the allow_write block.

So for both use cases described in the PR:

  1. sx --deny-write "$PWD" (running without write access to the working directory) → the working-dir allow file* comes later and wins, writes are still permitted.
  2. Denying a subpath of an --allow-write tree → the allow_write rules come later and win for nested paths.

The only paths where the deny currently "works" are ones that were never write-allowed anyway (the sandbox is deny-by-default), so as written the feature is effectively a no-op. This is exactly the invariant encoded in test_deny_rules_come_after_allow_read for the read side.

Fix: move the deny_write block to after the allow_write block in src/sandbox/seatbelt.rs. That placement also does the right thing semantically: the deny only targets file-write*, so read access inside the working dir is preserved.

2. Tests (blocking)

The project follows TDD, and deny_read has coverage in seatbelt_test.rs, cli_test.rs, and config_test.rs. Please add at minimum:

  • a rule-emission test (deny rule appears in the generated profile), and
  • an ordering test asserting the deny file-write* rules come after the working-dir and allow_write rules (mirroring test_deny_rules_come_after_allow_read) — this test would have caught issue ci: bump actions/github-script from 7 to 8 #1.

3. --explain output

print_explain in src/cli/commands.rs prints "Denied Read Paths" and "Allowed Write Paths" but not the new deny-write list. Please add a "Denied Write Paths" section so users can inspect the effective rules.

4. Nits

  • README table: "Deny write (override allows)" → "overrides allows" to match the --deny-read row.
  • One thing worth verifying on macOS once the ordering is fixed: when an allow_write path is an existing file, the profile emits a regex-filter allow, while deny_write emits a subpath-filter deny — mixed filter types may not follow plain last-match-wins. A quick manual check (or test) for that corner case would be great.

(The secrets-scan CI failure looks like the known gitleaks-on-fork-PR issue, not something in your change.)

Again, thank you — the feature is wanted and the structure is right; once the deny block is moved after the allows and covered by tests, this should be good to go. 🚀

@jaredly

jaredly commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@Pierozi thanks for the review! I've made the requested changes

@Pierozi

Pierozi commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Thanks for turning this around so quickly, @jaredly! 🎉 I re-reviewed a958e86 and everything checks out:

  • Rule ordering — the deny_write block now comes after both the working-directory rule and the allow_write block. I verified the generated profile for your primary use case, and it emits exactly what we want:
    (allow file* (subpath "<cwd>"))
    (deny file-write* (subpath "<cwd>"))
    
    Matching filter types with the deny last, so last-match-wins applies — writes denied, reads preserved. sx --deny-write "$PWD" now does what it says. ✅
  • test_deny_rules_come_after_allow_write — nicely mirrors the read-side test, and it's precisely the test that would have caught the original ordering bug. ✅
  • --explain now surfaces a "Denied Write Paths" section. ✅
  • README wording fixed. ✅

Full suite passes locally (245 tests), and lint / test / build are green in CI.

The red checks (secrets-scan, dependency-check, security-audit) are all pre-existing infrastructure issues unrelated to your change — a missing GITLEAKS_LICENSE secret, and RUSTSEC-2026-0190 in a transitive dependency that also fails on main. Your PR touches no Cargo.toml/Cargo.lock, so none of that is on you. I'll track those separately.

Two non-blocking follow-ups I'll pick up rather than hold this PR for:

  1. The mixed filter-type corner case — when allow_write targets an existing file, we emit a regex allow against a subpath deny, and last-match-wins is only guaranteed for matching filter types. Needs a macOS spot-check.
  2. An ordering test for the working-dir + deny_write pairing specifically, so a future reorder can't silently regress your use case.

Merging now — thanks again for the clean, well-scoped contribution and for being so responsive to review feedback. This is a genuinely useful addition to sx. 🙏

@Pierozi
Pierozi merged commit b765907 into agentic-dev3o:main Sep 18, 2026
3 of 6 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