Skip to content

cli: --violations writes each recorded violation to a file - #601

Open
carlostapiacl wants to merge 1 commit into
anthropics:mainfrom
carlostapiacl:cli-violations-flag
Open

carlostapiacl wants to merge 1 commit into
anthropics:mainfrom
carlostapiacl:cli-violations-flag

Conversation

@carlostapiacl

@carlostapiacl carlostapiacl commented Sep 23, 2026 •

Copy link
Copy Markdown

Closes #582.

The problem

A caller that runs srt can't see what the sandbox refused. The violation store is filled, but nothing on the CLI path reads it, and cli.ts calls initialize(runtimeConfig) without the third argument, so the kernel monitors never start and filesystem denies aren't recorded at all. The child just gets Operation not permitted.

The change

--violations <path>: opt-in, one flag.

  • Passes enableLogMonitor = true to initialize(), so the existing monitors run.
  • Subscribes to the store and appends each new violation to the file as a JSON line: timestamp, line, and command when known. The store keeps only its last 100 and notifies with all of them, so getTotalCount() decides which are new.
  • After the command exits, waits 250 ms before srt exits. Measured on macOS: exiting at once lost 9 of 10 denies from a command refused on its last line (sh -c 'echo x > outside'); deferring the exit kept 10 of 10.
  • A failed write is reported once on stderr and never stops the command.

Without the flag nothing changes: no monitor, no subscription, same exit timing.

Measured

macOS 15.7 (x86_64), with allowWrite holding /private/tmp/srt-demo/project and example.com as the only allowed host:

$ srt --settings settings.json --violations violations.jsonl \
    -c 'echo x > /private/tmp/srt-demo/outside.txt; curl -s -o /dev/null --max-time 5 https://github.com'
$ cat violations.jsonl
{"timestamp":"2026-09-23T21:27:44.749Z","line":"bash(28975) deny(1) file-write-create /private/tmp/srt-demo/outside.txt","command":"echo x > /private/tmp/srt-demo/outside.txt; curl -s -o /dev/null --max-time 5 https://github.com"}
{"timestamp":"2026-09-23T21:27:44.769Z","line":"curl(28978) deny(1) mach-lookup com.apple.SystemConfiguration.configd","command":"echo x > /private/tmp/srt-demo/outside.txt; curl -s -o /dev/null --max-time 5 https://github.com"}
{"timestamp":"2026-09-23T21:27:44.778Z","line":"deny network-outbound github.com:443 (host is not on the allow list)","command":"echo x > /private/tmp/srt-demo/outside.txt; curl -s -o /dev/null --max-time 5 https://github.com"}

The refused write and the kernel's mach-lookup deny come from the log monitor; the last line is the proxy's.

Linux (Debian 12, bwrap, Docker): the proxy deny is recorded; the refused write is not, because bwrap refuses it as Read-only file system and no monitor reports it. The README says so.

Tests

Three in test/cli.test.ts: a refused write is recorded even on the command's last line (macOS), a host the proxy refused is recorded (macOS and Linux), and no file appears when nothing is refused. All three fail without the change.

typecheck, lint, prettier and the Node fallback test pass. Full suite on macOS x86_64: 1207 pass, 680 skip, 1 fail. The failure is in symlink-boundary.test.ts and fails on main too on this machine (one or two of its tests, depending on timing): other processes were running under srt, whose default TMPDIR is /tmp/claude, and those tests delete and recreate /tmp/claude.

Exit codes are unchanged with the flag (checked exit 0, exit 3, SIGKILL, SIGTERM against the same commands without it).

Not covered: Windows. The flag still records proxy denies there; initialize() only starts the monitors on macOS and Linux.

Related but different: #597 changes what the sandboxed client is told when the proxy denies it. This is about the caller outside the sandbox.

A caller that runs the binary could not read the violation store, and on
the CLI path the kernel monitors were never started, so filesystem denies
were not recorded at all. --violations <path> turns the monitors on and
appends each violation as a JSON line (timestamp, line, command).

With the flag, srt waits 250 ms after the command exits: the monitor
delivers a deny a few milliseconds after the syscall, and exiting at once
lost 9 of 10 denies from commands refused on their last line (macOS).
Without the flag nothing changes.

Closes anthropics#582.

This branch has not been deployed

No deployments
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.

cli: filesystem violations are never collected, and collected ones never reach a caller that runs the binary

1 participant