Skip to content

fix(delivery): avoid E2BIG on large settings.local.json (#95)#97

Merged
fujibee merged 2 commits into
mainfrom
fix/delivery-e2big
Jun 10, 2026
Merged

fix(delivery): avoid E2BIG on large settings.local.json (#95)#97
fujibee merged 2 commits into
mainfrom
fix/delivery-e2big

Conversation

@fujibee

@fujibee fujibee commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #95. Reworks apply_settings() so the settings JSON is read via sqlite3's readfile() from a temp file at each stage instead of being interpolated into the sqlite3 argv. The old in-memory chain embedded the blob 6× in strip_agmsg_event (and 4× in the add/prune helpers), so settings files above ~21 KB tripped Linux's MAX_ARG_STRLEN (131072 bytes per single argv element) and delivery.sh set died with E2BIG.

Affected agent types: claude-code and codex (both use the generic apply_settings() path). gemini / antigravity (markdown) and copilot (template rewrite via rm -f + heredoc) are not affected.

Diff shape

  • strip_agmsg_eventstrip_agmsg_event_file (reads & writes a path; SQL references readfile('$path'))
  • add_event_entryadd_event_entry_file
  • prune_empty_hooksprune_empty_hooks_file
  • read_settings_escaped removed — no callers left.
  • apply_settings: in-memory $settings_esc chain → temp-file chain. The user's settings.local.json only changes at the end of the chain via mv, so a mid-chain failure no longer leaves a partially-modified file.
  • delivery.sh status: the three "count entries" probes also switched to readfile() (they had a much higher threshold — ~130 KB — but it cost nothing to fix while in the file).

No behavior change for normal-sized settings. The functions take paths instead of strings, but they only had internal callers.

Tests

Three new regression cases in tests/test_delivery.bats:

  • delivery set monitor on a ~25 KB settings.local.json (matches the reporter's ~32 KB case scaled down to stay portable). 25 KB × 6 ≈ 150 KB > Linux's per-arg cap, so the test fails on Linux pre-fix.
  • delivery set both — exercises the longest chain (3× strip + 3× add + 1× prune).
  • delivery set off — exercises strip-only, with an existing agmsg-owned entry plus 600 user permissions, confirming idempotent strip on a large file.

All three preserve and verify the user's permissions.allow array length (600 entries) across the rewrite.

Full suite: 71/71 pass locally (68 existing + 3 new).

Platform notes

  • Linux: MAX_ARG_STRLEN is fixed at 32 pages = 131072 bytes per single argv element, regardless of ARG_MAX. This is where the reporter hit the wall.
  • macOS: kern.argmax ≈ 1 MB; a 32 KB settings file (6× = 192 KB) survives there, so the bug never surfaced for mac-only developers. The new tests still pass on macOS post-fix.

Test plan

  • CI bats run goes green on Linux (where pre-fix the new tests would have failed).
  • Manual: run delivery.sh set monitor against a project with a settings file ≥ 25 KB on Linux; verify no E2BIG and the existing permissions block is intact.
  • Manual: delivery.sh status against the same file shows correct hook entry counts.

Closes #95.

fujibee added 2 commits June 10, 2026 13:05
The previous in-memory chain in apply_settings() embedded the full
settings JSON blob into single sqlite3 argv elements — six times in
strip_agmsg_event, four times in add_event_entry, four times in
prune_empty_hooks. On Linux the per-argument cap MAX_ARG_STRLEN is
131072 bytes, so settings files above ~21 KB caused 'set' to die with
E2BIG before sqlite3 even started.

Rework the chain so each stage reads from a temp file via sqlite3's
readfile() and writes back to that file, then atomically mv the final
state over the user's settings.local.json. The settings blob never
appears in argv any more, so the only ceiling left is sqlite3's own
limits (well past anything realistic).

- strip_agmsg_event   -> strip_agmsg_event_file
- add_event_entry     -> add_event_entry_file
- prune_empty_hooks   -> prune_empty_hooks_file
- read_settings_escaped removed (no callers left)
- delivery.sh status also switched to readfile() for the count probes

Tests: add three regression cases for set monitor / set both / set off
operating on a ~25 KB settings.local.json. 25 KB * 6 = 150 KB which is
> Linux's MAX_ARG_STRLEN; pre-fix the tests fail with E2BIG on Linux
(macOS has ~1 MB per-arg headroom so it survives 25 KB even pre-fix,
but the new tests still pass post-fix everywhere). All three confirm
the user's permissions.allow array is preserved across the rewrite.

Closes #95.
agmsg's value prop includes 'no python required'; the test should
follow the same constraint. Build the inflated permissions.allow JSON
via sqlite3 json_set instead of a python3 heredoc so the test runs in
minimal containers (Debian slim, distroless-style) where python3 isn't
preinstalled.
@fujibee fujibee merged commit 6711efc into main Jun 10, 2026
1 check passed
@fujibee

fujibee commented Jun 10, 2026

Copy link
Copy Markdown
Owner Author

Linux verification (record)

Verified on a Debian 12 container after the python3-free test rewrite (a231bb7), before merge.

Environment

OS Debian 12 bookworm
kernel 6.10.14-linuxkit (aarch64)
sqlite3 3.40.1 (readfile / fileio extension OK)
bats 1.8.2
getconf ARG_MAX 2097152 (2 MB total argv+envp)
MAX_ARG_STRLEN 131072 (per-arg cap, compile-time)

Post-fix run

$ bats tests/test_delivery.bats --filter "E2BIG|#95"
ok 1 delivery set monitor: handles a large settings.local.json without E2BIG (#95)
ok 2 delivery set both: handles a large settings.local.json across strip+add+prune (#95)
ok 3 delivery set off: idempotent strip on a large settings.local.json (#95)

Pre-fix simulation

Reverted scripts/delivery.sh to main while keeping the new tests:

$ git checkout main -- scripts/delivery.sh
$ bats tests/test_delivery.bats --filter "E2BIG|#95"
not ok 1 delivery set monitor: handles a large settings.local.json without E2BIG (#95)
not ok 2 delivery set both: handles a large settings.local.json across strip+add+prune (#95)
not ok 3 delivery set off: idempotent strip on a large settings.local.json (#95)

All three regression tests fail pre-fix, confirming they would have caught the original bug.

Raw E2BIG reproduction (sanity)

Direct invocation of delivery.sh set monitor against a 25,227-byte settings.local.json:

$ bash scripts/delivery.sh set monitor claude-code /tmp/e2big-repro
scripts/delivery.sh: line 64: /usr/bin/sqlite3: Argument list too long

Line 64 (pre-fix) is the sqlite3 invocation inside strip_agmsg_event — matches the reporter's diagnosis (6× argv embedding) exactly. 6 × 25227 = 151362 bytes > MAX_ARG_STRLEN (131072).

Sanity restore

$ git checkout fix/delivery-e2big -- scripts/delivery.sh
$ bats tests/test_delivery.bats --filter "E2BIG|#95"
ok 1 ... ok 2 ... ok 3 ...

Back to 3/3 green. Safe to merge.

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.

delivery.sh set fails with E2BIG when settings.local.json exceeds ~21KB (settings embedded 6x into one sqlite3 argv)

1 participant