fix(assemble): an apostrophe glued to a ${ or the closing '' (#244) - #304
Merged
Conversation
nix_escape() applied its two rules as two passes: `''`→`'''`, then
`${`→`''${`. Both are right alone, and wrong together — the escape for
`${` starts with `''`, and Nix lexes a run of apostrophes greedily three
at a time. So an ordinary shell line
echo "key '${LOCAL_WEBHOOK_SPAWN_KEY:-event}' is unusable" >&2
came out as `'` + `''${`, which Nix reads as an escaped `''` plus a LIVE
antiquotation: `nix run .#assemble` succeeded and every later `nix build`
failed with `undefined variable 'event'` pointing into the generated
module, with nothing wrong in the source file. With a `${FOO}` (no
default) the payload is silently mangled instead — that half ships.
Escape the run and the `${` behind it in one pass: pairs become `'''`,
and an odd trailing apostrophe becomes `''\'`, which leaves the `''${`
unambiguous. Real Nix has the same problem at the other end of the
payload — a text ending in an odd apostrophe run glues onto the host's
closing `''` and the string never terminates — so that case is escaped
too, which makes the result splice-anywhere safe.
tests/test-assemble-module.py decodes the escaped text the way Nix's
IND_STRING rules do and round-trips every string up to 5 characters over
`' $ { } \ a` (9331 of them) through it; the model was validated by
running the same corpus through `nix-instantiate --eval --json`, which
`--nix` re-runs on demand. module-generated-up-to-date cannot catch an
escaping bug on its own: it regenerates the file with the same
assembler, so the check and the bug agree on the wrong bytes.
modules/src/webhook-spawn.sh gets its natural wording back (the #243
workaround printed the key unquoted), so the generated module now
carries the escaped form and every eval check parses it for real.
Closes #244
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsqGhkpjsb4kKAdVz25xKN
lionello
approved these changes
Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
bin/assemble-module.py'snix_escape()escaped in two passes —''→''', then${→''${. Each rule is right alone and wrong in combination: the escape for${starts with'', and Nix lexes a run of apostrophes greedily, three at a time. So an ordinary shell linereached the generated module as
'+''${…, which Nix reads as an escaped''plus a live antiquotation.nix run .#assemblesucceeded; every laternix buildfailed witherror: undefined variable 'event'pointing intomodules/agent-box.nix, with nothing wrong in the source file. With a${FOO}(no:-default) there is no undefined variable and the payload is silently mangled instead — that half ships. Found in #243, which worked around it by rewording the line. Fixes #244.Real Nix showed the same flaw at the other end of a payload: text ending in an odd number of apostrophes glues onto the host's closing
''and the string never terminates.What changed
bin/assemble-module.py— one pass over'+\$\{|'+|\$\{, so a quote run and the${behind it are escaped together. Pairs become'''; an odd trailing apostrophe becomes''\', which leaves the following''${(or the host's closing'') unambiguous. The result is now splice-anywhere safe.tests/test-assemble-module.py(new) — decodes escaped text the way Nix'sIND_STRINGlexer rules do (raising on a live${, an unterminated string, or trailing text), and round-trips every string up to 5 characters over' $ { } \ a— 9331 of them — throughnix_escapeand back. Plus explicit expected escapes, the exact fix(sessions): let a session name be as long as a dispatch can mint #243 line, and tworesolve()-level tests for@@include:vs@@include-verbatim:. One test re-runs the corpus through the old two-pass escaping and asserts it fails, so a decoder that accepted everything would not pass silently.flake.nix— new eval-level checkassemble-module-escaping(both systems), plus a CI step.module-generated-up-to-datecannot catch an escaping bug: it regenerates the file with the same assembler, so the check and the bug agree on the wrong bytes.modules/src/webhook-spawn.sh— the fix(sessions): let a session name be as long as a dispatch can mint #243 workaround (key printed unquoted, with a comment pointing at assemble-module.py: a single quote directly before ${ escapes wrong — silent corruption or a broken generated module #244) is replaced by the natural wording. This is also the regression test with real Nix in it: the committed module now carries''\'''${LOCAL_WEBHOOK_SPAWN_KEY:-event}', somodule-single-file,vm-closureandgolden-snapshotall parse it. The golden fixture shows the rendered script back with the apostrophes in place.The model is validated against real Nix
The committed test uses a Python model of the lexer, because the check runs in a build sandbox with no
nix. The model was checked against the real thing on this box — all 9331 escaped strings evaluate back to their source throughnix-instantiate --eval --json:--nixis kept in the file so anyone can redo that.User-visible and security effects
None on a deployed box beyond one diagnostic's wording.
agent-box-webhook-spawnnow saysThe escaping change is a no-op for every payload in the tree today (no
modules/src/*file contains the pattern — one could not, the tree would not build), somodules/agent-box.nixchanges only by that reworded line.Checks run
On this
aarch64-linuxbox (all exit 0):assemble-module-escaping— 7 tests OKpython3 tests/test-assemble-module.py --nix— 9331/9331 through real Nixmodule-generated-up-to-date,module-single-file,multi-user,download-route,webhook-routegolden-snapshot— failed with exactly the one expected diagnostic diff, regenerated withnix run .#update-golden, green afterVM tests and
vm-closurearex86_64-linux-only; CI covers them.