A value on bwrap's argv stands in /proc/<pid>/cmdline and in every ps;
the values are the secrets of a build. bwrap's own environment is no
alternative: an LD_PRELOAD meant for the sandbox would apply to bwrap
on the host.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-code
marked the pull request as ready for review 2026-10-20 17:16:53 +02:00
YELLOW Sandbox isolation according to Jev: internal/engine/bwrap.go (0.90); a human checks it, unless you revert the change because the task does not need it
hint writing rules violated in the changed docs: docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md:9: line longer than 128 characters (153); docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md:23: line longer than 128 characters (147); docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md:81: line longer than 128 characters (134)
hint 2 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev
hint 2 finding(s) of low severity or rated minor by Jev: internal/engine/bwrap.go:47-52, internal/engine/bwrap.go:142-144
Findings
[low] Awkward wording in code comment at internal/engine/bwrap.go:47-52, hint, as a line comment
[low] Error message ambiguity at internal/engine/bwrap.go:142-144, hint, as a line comment
werkrevisor: YELLOW / HUMAN_REVIEW for fa1b5521ef3f077d3b6b4539d79bd17374be6095.
The result comment of werkrevisor holds the details.
werkrevisor: YELLOW / HUMAN_REVIEW for `fa1b5521ef3f077d3b6b4539d79bd17374be6095`.
The result comment of werkrevisor holds the details.
<!-- werkrevisor-review:fa1b5521ef3f077d3b6b4539d79bd17374be6095 -->
- [werkator PR#166](https://git.javagil.de/mi/werkator/pulls/166): Werkator passes the values of a build's variables to werkdock through its environment.
[low] Awkward wording in code comment (werkrevisor)
hint
The new Argv doc comment contains the ungrammatical phrase "bwrap's own environment is no way either", which is hard to parse in context. The sentence is trying to explain that setting the environment through bwrap's own variables is also rejected, because bwrap runs on the host and an LD_PRELOAD meant for the sandbox would load into bwrap itself. A phrasing such as "bwrap's own environment is no better" or "setting it through bwrap's environment is no option either" would convey the intent to a future reader.
**[low] Awkward wording in code comment** (werkrevisor)
hint
The new Argv doc comment contains the ungrammatical phrase "bwrap's own environment is no way either", which is hard to parse in context. The sentence is trying to explain that setting the environment through bwrap's own variables is also rejected, because bwrap runs on the host and an LD_PRELOAD meant for the sandbox would load into bwrap itself. A phrasing such as "bwrap's own environment is no better" or "setting it through bwrap's environment is no option either" would convey the intent to a future reader.
When EnvArgs refuses an environment variable for containing a NUL byte, the error message reads "environment variable %q holds a NUL byte" and prints only the key name. If the NUL byte is in the value while the key is well-formed, the message attributes the NUL to the key side and misleads the operator. A realistic scenario: a caller runs -e KEY=val<NUL>tail, gets the error "environment variable "KEY" holds a NUL byte", and looks for the NUL in the key when it is actually in the value.
**[low] Error message ambiguity** (werkrevisor)
hint
When EnvArgs refuses an environment variable for containing a NUL byte, the error message reads "environment variable %q holds a NUL byte" and prints only the key name. If the NUL byte is in the value while the key is well-formed, the message attributes the NUL to the key side and misleads the operator. A realistic scenario: a caller runs `-e KEY=val<NUL>tail`, gets the error "environment variable \"KEY\" holds a NUL byte", and looks for the NUL in the key when it is actually in the value.
ai-review/assessment AI findings; a developer approval after checking them unlocks the merge; a human review request is pending; merge already scheduled
config.json holds the whole environment of the sandbox, also for run
--rm; with 0644 in a 0755 directory, the other accounts of the group
read the secrets of a running build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-code
changed title from The sandbox's environment no longer stands on the command line of bwrap (0.13.2) to The sandbox's environment no longer stands on bwrap's command line, nor in a file others can read (0.13.2)2026-10-20 17:23:18 +02:00
ORANGE 2 finding(s) that Jev rates as probably real and severe: internal/engine/bwrap.go:401-404, docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md:22-24
YELLOW Sandbox isolation according to Jev: internal/engine/bwrap.go (0.91); a human checks it, unless you revert the change because the task does not need it
hint writing rules violated in the changed docs: docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md:9: line longer than 128 characters (153); docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md:27: line longer than 128 characters (147)
hint 5 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev
hint 1 finding(s) of low severity or rated minor by Jev: internal/engine/bwrap.go:112-114
hint model recommends changes before merge
Findings
Ignored write error at internal/engine/bwrap.go:401-404, severity unknown, counted as medium, ORANGE, as a line comment
Contradicts the migration story at docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md:22-24, severity unknown, counted as medium, ORANGE, as a line comment
EnvArgs called twice at internal/engine/bwrap.go:112-114, severity unknown, counted as medium, hint, as a line comment
8 of 9 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 46731, calls 1, tokens in 45108 / out 6310 (reasoning included in out), duration 80.9 s, cost 0.0211 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7369, cost 0.0003 USD, finding judge `jev-1.13.0` calls 3, tokens in 5913, cost 0.0002 USD, build file judge `jev-1.13.0` calls 6, tokens in 8400, cost 0.0004 USD, phases: path_judge 0.5 s, classifier 0.6 s, context 0.3 s, model_review 78.8 s, finding_judge 0.3 s, config: defaults + host + repository (.werkrevisor.toml at main 03312d4).
Reviewed head `afdf9ac1c74438907de8861ba0fb0a2457a2d0f2` against base `03312d484ee61a4a64cec000a474a082ce0b6581`, werkrevisor 0.54.1.
<!-- werkrevisor:afdf9ac1c74438907de8861ba0fb0a2457a2d0f2 -->
<!-- werkrevisor-verdict:ORANGE -->
<!-- werkrevisor-blocks:3b3b4ee3ba528b1e9ee560897b1eed60,425f4c4d3da745c8a0fd40b979256fb6,bad207dfba7581257d047309bc8c74f5,39c0cb52f4a27b5ef5ff246afc2bf801,e3609263a380d7f5812af57b663d9379,7e857faac973e81e7f419b50f9e92714,38f8c9a83df53e47c05d6204b4f2d823,9fe277402ecfdf2972bdb6bcb6504774,0ae918fabd5cdae037e9b64dda3853f2,46680c9b823ae436f4e8177079058e00,3f032dcfc15bab49c79258805befe3d7,6be94e0c3b0a61b22065c494dbd58b4d,51bef0c97b0221380692d0a5bf191333,de71cb042d0bf6bd66579c0169d23bc1,cc8f40c6622a5545a1e1ac2c6dda7433,73a9159f75162d696e36dafaccb2a63c,48bda00a7630508802be4fde75a60730,5e51e3e31ba0d8e2805f070d4f35537b,fa68d266f5df6dcb039c6fec762a90dc,948ebe38e82becb173ebd2f13a5958ae,57dc07fc962fd5d07541bea8c0591696,1da8b8469ca64a4052c059daf1b46234,1d4eed3af913420dff7e1b5dd01762af,6a0e457324b55cbea4b465267bb41adc,b32a0c192f1bf4ce2b1db65ef40d64b2,5c7bbfa26df36a03bdc493eb19b0e63c,d6491cdba43b60bbac87cc150ac87e26,762786a53e02ee2b081f2609f488fdc9,6bff4c7ea6416304e9d54786d894a9ea,feb14579811ef3df0a6af3c5c566242e,c34c3f418b03bbb57c676c1d645231bc -->
<!-- werkrevisor-folded -->
<details><summary>🟠 werkrevisor ORANGE for <code>afdf9ac</code>, superseded by <a href="http://git.javagil.de/mi/werkdock/pulls/17#issuecomment-6800">the result for <code>9402adc</code></a></summary>
## 🟠 werkrevisor: ORANGE / FINDINGS
Change kind: `mixed`.
### Reasons
- **ORANGE** 2 finding(s) that Jev rates as probably real and severe: internal/engine/bwrap.go:401-404, docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md:22-24
- **YELLOW** risky paths changed: docs/risk-analysis.md (docs/risk-analysis.md)
- **YELLOW** Sandbox isolation according to Jev: internal/engine/bwrap.go (0.91); a human checks it, unless you revert the change because the task does not need it
- **YELLOW** spec: docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md Scenario#17.02 hard_to_read (0.53 > yellow_above = 0.5)
- **hint** writing rules violated in the changed docs: docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md:9: line longer than 128 characters (153); docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md:27: line longer than 128 characters (147)
- **hint** 5 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev
- **hint** 1 finding(s) of low severity or rated minor by Jev: internal/engine/bwrap.go:112-114
- **hint** model recommends changes before merge
### Findings
1. **Ignored write error** at `internal/engine/bwrap.go:401-404`, severity unknown, counted as medium, ORANGE, as a line comment
2. **Contradicts the migration story** at `docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md:22-24`, severity unknown, counted as medium, ORANGE, as a line comment
3. **EnvArgs called twice** at `internal/engine/bwrap.go:112-114`, severity unknown, counted as medium, hint, as a line comment
### Classifier
| Unit | Question | Problem | Confidence | Verdict |
|---|---|---|---|---|
| docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md Scenario#17.02 | hard_to_read | 0.53 | 0.53 | YELLOW |
8 of 9 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
<sub>
Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 46731, calls 1, tokens in 45108 / out 6310 (reasoning included in out), duration 80.9 s, cost 0.0211 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7369, cost 0.0003 USD, finding judge `jev-1.13.0` calls 3, tokens in 5913, cost 0.0002 USD, build file judge `jev-1.13.0` calls 6, tokens in 8400, cost 0.0004 USD, phases: path_judge 0.5 s, classifier 0.6 s, context 0.3 s, model_review 78.8 s, finding_judge 0.3 s, config: defaults + host + repository (.werkrevisor.toml at main 03312d4).<br>
Reviewed head `afdf9ac1c74438907de8861ba0fb0a2457a2d0f2` against base `03312d484ee61a4a64cec000a474a082ce0b6581`, werkrevisor 0.54.1.
</sub>
</details>
werkrevisor: ORANGE / FINDINGS for afdf9ac1c74438907de8861ba0fb0a2457a2d0f2.
The result comment of werkrevisor holds the details.
werkrevisor: ORANGE / FINDINGS for `afdf9ac1c74438907de8861ba0fb0a2457a2d0f2`.
The result comment of werkrevisor holds the details.
<!-- werkrevisor-review:afdf9ac1c74438907de8861ba0fb0a2457a2d0f2 -->
Contradicts the migration story, severity unknown, counted as medium (werkrevisor)
ORANGE; Jev: real 0.79, severity 3.0 of 3
Section "For consumers" in CHANGELOG.md tells consumers to "Pass a secret as a bare -e KEY with the value in werkdock's own environment, as Werkator does since werkator PR#166". The PR document's Solution section says the secrets end up in instances/<id>/config.json and describes tightening its mode, but never addresses that this very file still serializes the full environment to disk on run --rm and run -d. The filesystem-permission change closes the world-readable hole, but config.json still holds every secret in plaintext at rest — a fact the PR document should acknowledge alongside the mode tightening rather than implicitly claiming the secrets are merely off the command line.
**Contradicts the migration story**, severity unknown, counted as medium (werkrevisor)
ORANGE; Jev: real 0.79, severity 3.0 of 3
Section "For consumers" in CHANGELOG.md tells consumers to "Pass a secret as a bare `-e KEY` with the value in werkdock's own environment, as Werkator does since werkator PR#166". The PR document's Solution section says the secrets end up in `instances/<id>/config.json` and describes tightening its mode, but never addresses that this very file still serializes the full environment to disk on `run --rm` and `run -d`. The filesystem-permission change closes the world-readable hole, but config.json still holds every secret in plaintext at rest — a fact the PR document should acknowledge alongside the mode tightening rather than implicitly claiming the secrets are merely off the command line.
Fixed in 38550a0: the Non-Goals say that config.json keeps the environment in plain text, why (start of a stopped instance needs it), and that mode 0600 limits it to the account, which holds the secrets anyway.
Fixed in `38550a0`: the Non-Goals say that config.json keeps the environment in plain text, why (`start` of a stopped instance needs it), and that mode 0600 limits it to the account, which holds the secrets anyway.
EnvArgs called twice, severity unknown, counted as medium (werkrevisor)
hint; Jev: real 0.70, severity 1.9 of 3
Argv calls EnvArgs(spec) to validate (lines 112-114) and discards the returned byte slice, then Start (lines 365-367) calls EnvArgs(spec) again to get the actual payload. Iterating environment(home, spec.Env) and checking NUL bytes runs twice per sandbox for no reason. Either have Argv return the payload, or pass through the already-validated slice; otherwise the work is duplicated for every sandbox start.
**EnvArgs called twice**, severity unknown, counted as medium (werkrevisor)
hint; Jev: real 0.70, severity 1.9 of 3
`Argv` calls `EnvArgs(spec)` to validate (lines 112-114) and discards the returned byte slice, then `Start` (lines 365-367) calls `EnvArgs(spec)` again to get the actual payload. Iterating `environment(home, spec.Env)` and checking NUL bytes runs twice per sandbox for no reason. Either have `Argv` return the payload, or pass through the already-validated slice; otherwise the work is duplicated for every sandbox start.
Stays, on purpose: Argv is also what werkdock run validates a spec with before it creates the instance (internal/cli/run.go:108), so it must refuse a NUL byte as Start would. A second pass over a few variables costs microseconds per sandbox start.
Stays, on purpose: `Argv` is also what `werkdock run` validates a spec with before it creates the instance (internal/cli/run.go:108), so it must refuse a NUL byte as `Start` would. A second pass over a few variables costs microseconds per sandbox start.
Ignored write error, severity unknown, counted as medium (werkrevisor)
ORANGE; Jev: real 0.81, severity 2.9 of 3
In Start, the goroutine writes the environment payload to the pipe with envW.Write(env); envW.Close() and ignores the returned error. While Go's (*os.File).Write is documented to return a non-nil error when n != len(b) (so a silent truncation on a single call is rare), a write error here signals that bwrap cannot or will not receive the full environment — leaving the sandbox with missing variables and no surfaced failure. With a NUL-framed protocol this is also a correctness issue: a partial write that returns an error drops trailing variables rather than producing a malformed frame, but the supervisor proceeds as if Start succeeded. Loop until all bytes are written or handle the error.
**Ignored write error**, severity unknown, counted as medium (werkrevisor)
ORANGE; Jev: real 0.81, severity 2.9 of 3
In `Start`, the goroutine writes the environment payload to the pipe with `envW.Write(env); envW.Close()` and ignores the returned error. While Go's `(*os.File).Write` is documented to return a non-nil error when n != len(b) (so a silent truncation on a single call is rare), a write error here signals that bwrap cannot or will not receive the full environment — leaving the sandbox with missing variables and no surfaced failure. With a NUL-framed protocol this is also a correctness issue: a partial write that returns an error drops trailing variables rather than producing a malformed frame, but the supervisor proceeds as if Start succeeded. Loop until all bytes are written or handle the error.
Fixed in 38550a0: a failed write kills bwrap before the pipe closes. bwrap reads fd 4 up to EOF before it does anything else, so it never starts a sandbox with part of its environment; Start then fails on the missing child pid.
Fixed in `38550a0`: a failed write kills bwrap before the pipe closes. bwrap reads fd 4 up to EOF before it does anything else, so it never starts a sandbox with part of its environment; Start then fails on the missing child pid.
ai-review/assessment AI findings; a developer approval after checking them unlocks the merge; a human review request is pending; merge already scheduled
YELLOW Sandbox isolation according to Jev: internal/engine/bwrap.go (0.89); a human checks it, unless you revert the change because the task does not need it
8 of 9 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 46920, calls 1, tokens in 45286 / out 28445 (reasoning included in out), duration 342.4 s, cost 0.0468 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7419, cost 0.0003 USD, finding judge `jev-1.13.0` calls 1, tokens in 1985, cost 0.0001 USD, build file judge `jev-1.13.0` calls 6, tokens in 8460, cost 0.0004 USD, phases: path_judge 0.4 s, classifier 0.6 s, context 0.3 s, model_review 340.3 s, finding_judge 0.2 s, config: defaults + host + repository (.werkrevisor.toml at main 03312d4).
Reviewed head `9402adc0f93126f6ba6d914ac59932240ebc820e` against base `03312d484ee61a4a64cec000a474a082ce0b6581`, werkrevisor 0.54.1.
<!-- werkrevisor:9402adc0f93126f6ba6d914ac59932240ebc820e -->
<!-- werkrevisor-verdict:ORANGE -->
<!-- werkrevisor-blocks:3b3b4ee3ba528b1e9ee560897b1eed60,e7297cbf3bbb04bf7b8c1abea128d8fa,bad207dfba7581257d047309bc8c74f5,39c0cb52f4a27b5ef5ff246afc2bf801,e3609263a380d7f5812af57b663d9379,61dbd255093aa446ae5571b265e8310e,38f8c9a83df53e47c05d6204b4f2d823,9fe277402ecfdf2972bdb6bcb6504774,c6f8e5534d7a2efeb4c7475aac8b2352,46680c9b823ae436f4e8177079058e00,3f032dcfc15bab49c79258805befe3d7,6be94e0c3b0a61b22065c494dbd58b4d,51bef0c97b0221380692d0a5bf191333,d7cac39f4a80835f9522c439e4f3025b,cc8f40c6622a5545a1e1ac2c6dda7433,73a9159f75162d696e36dafaccb2a63c,48bda00a7630508802be4fde75a60730,5e51e3e31ba0d8e2805f070d4f35537b,fa68d266f5df6dcb039c6fec762a90dc,948ebe38e82becb173ebd2f13a5958ae,57dc07fc962fd5d07541bea8c0591696,1da8b8469ca64a4052c059daf1b46234,1d4eed3af913420dff7e1b5dd01762af,6a0e457324b55cbea4b465267bb41adc,b32a0c192f1bf4ce2b1db65ef40d64b2,5c7bbfa26df36a03bdc493eb19b0e63c,d6491cdba43b60bbac87cc150ac87e26,762786a53e02ee2b081f2609f488fdc9,6bff4c7ea6416304e9d54786d894a9ea,feb14579811ef3df0a6af3c5c566242e,c34c3f418b03bbb57c676c1d645231bc -->
<!-- werkrevisor-folded -->
<details><summary>🟠 werkrevisor ORANGE for <code>9402adc</code>, superseded by <a href="http://git.javagil.de/mi/werkdock/pulls/17#issuecomment-6815">the result for <code>1860d07</code></a></summary>
## 🟠 werkrevisor: ORANGE / FINDINGS
Change kind: `mixed`.
### Reasons
- **ORANGE** 1 finding(s) that Jev rates as probably real and severe: internal/engine/bwrap.go:403-408
- **YELLOW** risky paths changed: docs/risk-analysis.md (docs/risk-analysis.md)
- **YELLOW** Sandbox isolation according to Jev: internal/engine/bwrap.go (0.89); a human checks it, unless you revert the change because the task does not need it
- **YELLOW** spec: docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md Scenario#17.02 hard_to_read (0.52 > yellow_above = 0.5)
- **hint** 5 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev
- **hint** model recommends merge with caution
### Findings
1. **SIGPIPE Risk** at `internal/engine/bwrap.go:403-408`, severity unknown, counted as medium, ORANGE, as a line comment
### Classifier
| Unit | Question | Problem | Confidence | Verdict |
|---|---|---|---|---|
| docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md Scenario#17.02 | hard_to_read | 0.52 | 0.50 | YELLOW |
8 of 9 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
<sub>
Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 46920, calls 1, tokens in 45286 / out 28445 (reasoning included in out), duration 342.4 s, cost 0.0468 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7419, cost 0.0003 USD, finding judge `jev-1.13.0` calls 1, tokens in 1985, cost 0.0001 USD, build file judge `jev-1.13.0` calls 6, tokens in 8460, cost 0.0004 USD, phases: path_judge 0.4 s, classifier 0.6 s, context 0.3 s, model_review 340.3 s, finding_judge 0.2 s, config: defaults + host + repository (.werkrevisor.toml at main 03312d4).<br>
Reviewed head `9402adc0f93126f6ba6d914ac59932240ebc820e` against base `03312d484ee61a4a64cec000a474a082ce0b6581`, werkrevisor 0.54.1.
</sub>
</details>
werkrevisor: ORANGE / FINDINGS for 9402adc0f93126f6ba6d914ac59932240ebc820e.
The result comment of werkrevisor holds the details.
werkrevisor: ORANGE / FINDINGS for `9402adc0f93126f6ba6d914ac59932240ebc820e`.
The result comment of werkrevisor holds the details.
<!-- werkrevisor-review:9402adc0f93126f6ba6d914ac59932240ebc820e -->
SIGPIPE Risk, severity unknown, counted as medium (werkrevisor)
ORANGE; Jev: real 0.76, severity 3.0 of 3
The goroutine writes the sandbox's environment to envW without ignoring SIGPIPE. If bwrap dies before or while reading fd 4 — a too-large value rejected by execve, an internal bwrap crash, or any external kill of bwrap — the subsequent envW.Write returns EPIPE and SIGPIPE terminates the werkdock process before the goroutine can run cmd.Process.Kill(). The go runtime only ignores SIGPIPE for net.Conn reads/writes; raw os.File writes do not have this protection. Add signal.Ignore(syscall.SIGPIPE) (at process startup or around the write) so EPIPE is handled in Go rather than killing werkdock and orphaning bwrap.
**SIGPIPE Risk**, severity unknown, counted as medium (werkrevisor)
ORANGE; Jev: real 0.76, severity 3.0 of 3
The goroutine writes the sandbox's environment to `envW` without ignoring SIGPIPE. If bwrap dies before or while reading fd 4 — a too-large value rejected by execve, an internal bwrap crash, or any external kill of bwrap — the subsequent `envW.Write` returns EPIPE and SIGPIPE terminates the werkdock process before the goroutine can run `cmd.Process.Kill()`. The go runtime only ignores SIGPIPE for `net.Conn` reads/writes; raw `os.File` writes do not have this protection. Add `signal.Ignore(syscall.SIGPIPE)` (at process startup or around the write) so EPIPE is handled in Go rather than killing werkdock and orphaning bwrap.
No change; the finding does not hold for Go. The os/signal documentation, section "SIGPIPE": a write to a broken pipe on file descriptors 1 or 2 makes the program exit with SIGPIPE; on any other file descriptor the runtime takes no action on the signal, and the write fails with EPIPE. That holds for os.File, not only net.Conn. The env pipe is a fresh descriptor, never 1 or 2. Checked with go1.26.10: a program that writes to an os.Pipe whose read end is closed prints "write |1: broken pipe" and exits 0. So the Write returns the error, and the goroutine kills bwrap as intended.
No change; the finding does not hold for Go. The os/signal documentation, section "SIGPIPE": a write to a broken pipe on file descriptors 1 or 2 makes the program exit with SIGPIPE; on any other file descriptor the runtime takes no action on the signal, and the write fails with EPIPE. That holds for os.File, not only net.Conn. The env pipe is a fresh descriptor, never 1 or 2. Checked with go1.26.10: a program that writes to an os.Pipe whose read end is closed prints "write |1: broken pipe" and exits 0. So the Write returns the error, and the goroutine kills bwrap as intended.
YELLOW Sandbox isolation according to Jev: internal/engine/bwrap.go (0.90); a human checks it, unless you revert the change because the task does not need it
hint 5 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev
Classifier
9 of 9 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 46875, calls 1, tokens in 45244 / out 10306 (reasoning included in out), duration 125.6 s, cost 0.0259 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7419, cost 0.0003 USD, build file judge `jev-1.13.0` calls 6, tokens in 8460, cost 0.0004 USD, phases: path_judge 0.5 s, classifier 0.6 s, context 0.3 s, model_review 123.7 s, config: defaults + host + repository (.werkrevisor.toml at main 03312d4).
Reviewed head `1860d076fbf25f2d48ba4f5785e37144366b4ec1` against base `03312d484ee61a4a64cec000a474a082ce0b6581`, werkrevisor 0.54.1.
<!-- werkrevisor:1860d076fbf25f2d48ba4f5785e37144366b4ec1 -->
<!-- werkrevisor-verdict:YELLOW -->
<!-- werkrevisor-blocks:3b3b4ee3ba528b1e9ee560897b1eed60,e7297cbf3bbb04bf7b8c1abea128d8fa,bad207dfba7581257d047309bc8c74f5,39c0cb52f4a27b5ef5ff246afc2bf801,e3609263a380d7f5812af57b663d9379,61dbd255093aa446ae5571b265e8310e,38f8c9a83df53e47c05d6204b4f2d823,9fe277402ecfdf2972bdb6bcb6504774,c6f8e5534d7a2efeb4c7475aac8b2352,46680c9b823ae436f4e8177079058e00,3f032dcfc15bab49c79258805befe3d7,6be94e0c3b0a61b22065c494dbd58b4d,51bef0c97b0221380692d0a5bf191333,d7cac39f4a80835f9522c439e4f3025b,cc8f40c6622a5545a1e1ac2c6dda7433,73a9159f75162d696e36dafaccb2a63c,48bda00a7630508802be4fde75a60730,5e51e3e31ba0d8e2805f070d4f35537b,fa68d266f5df6dcb039c6fec762a90dc,948ebe38e82becb173ebd2f13a5958ae,57dc07fc962fd5d07541bea8c0591696,1da8b8469ca64a4052c059daf1b46234,1d4eed3af913420dff7e1b5dd01762af,6a0e457324b55cbea4b465267bb41adc,b32a0c192f1bf4ce2b1db65ef40d64b2,5c7bbfa26df36a03bdc493eb19b0e63c,d6491cdba43b60bbac87cc150ac87e26,762786a53e02ee2b081f2609f488fdc9,6bff4c7ea6416304e9d54786d894a9ea,feb14579811ef3df0a6af3c5c566242e,c34c3f418b03bbb57c676c1d645231bc -->
## 🟡 werkrevisor: YELLOW / HUMAN_REVIEW
Change kind: `mixed`.
### Reasons
- **YELLOW** risky paths changed: docs/risk-analysis.md (docs/risk-analysis.md)
- **YELLOW** Sandbox isolation according to Jev: internal/engine/bwrap.go (0.90); a human checks it, unless you revert the change because the task does not need it
- **hint** 5 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev
### Classifier
9 of 9 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
<sub>
Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 46875, calls 1, tokens in 45244 / out 10306 (reasoning included in out), duration 125.6 s, cost 0.0259 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7419, cost 0.0003 USD, build file judge `jev-1.13.0` calls 6, tokens in 8460, cost 0.0004 USD, phases: path_judge 0.5 s, classifier 0.6 s, context 0.3 s, model_review 123.7 s, config: defaults + host + repository (.werkrevisor.toml at main 03312d4).<br>
Reviewed head `1860d076fbf25f2d48ba4f5785e37144366b4ec1` against base `03312d484ee61a4a64cec000a474a082ce0b6581`, werkrevisor 0.54.1.
</sub>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
See docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md.
🤖 Generated with Claude Code
🟡 werkrevisor YELLOW for
fa1b552, superseded by the result forafdf9ac🟡 werkrevisor: YELLOW / HUMAN_REVIEW
Change kind:
mixed.Reasons
Findings
[low] Awkward wording in code comment at
internal/engine/bwrap.go:47-52, hint, as a line comment[low] Error message ambiguity at
internal/engine/bwrap.go:142-144, hint, as a line commentClassifier
7 of 9 units passed all questions (1 PR document(s)).
Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 30021, calls 1, tokens in 28981 / out 21111 (reasoning included in out), duration 264.9 s, cost 0.0340 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 6872, cost 0.0003 USD, finding judge `none` calls 0, tokens in 0, cost 0.0000 USD, build file judge `jev-1.13.0` calls 3, tokens in 5293, cost 0.0002 USD, phases: path_judge 0.3 s, classifier 0.7 s, context 0.2 s, model_review 263.4 s, finding_judge 0.0 s, config: defaults + host + repository (.werkrevisor.toml at main1 answer ignored because of a confidence below [classifier] min_confidence.
03312d4).Reviewed head `fa1b5521ef3f077d3b6b4539d79bd17374be6095` against base `03312d484ee61a4a64cec000a474a082ce0b6581`, werkrevisor 0.54.1.
werkrevisor: YELLOW / HUMAN_REVIEW for
fa1b5521ef3f077d3b6b4539d79bd17374be6095.The result comment of werkrevisor holds the details.
@@ -0,0 +6,4 @@## Related Links- [werkator PR#166](https://git.javagil.de/mi/werkator/pulls/166): Werkator passes the values of a build's variables to werkdock through its environment.Writing rule: line longer than 128 characters (153) (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
38550a0: the link line is wrapped.@@ -0,0 +20,4 @@## Non-Goals- No change of what the sandbox sees: the image's variables, the `-e` values and werkdock's `HOME` and `PATH`, nothing of the caller's environment.Writing rule: line longer than 128 characters (147) (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
38550a0: the Non-Goals line is wrapped.@@ -0,0 +78,4 @@## Follow-up PRs- The configuration of a detached instance (`run -d`, the API socket) holds its environment in a file of mode 0644 below `instances/`.Writing rule: line longer than 128 characters (134) (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed earlier, in
afdf9ac: the Follow-up section with that line is gone; no line is longer than 128 characters.@@ -44,6 +44,13 @@ const DefaultPATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bi// as they are in a container without --init. bwrap reports the// command's host pid on fd 3 (--info-fd), which Start reads.//// The environment is cleared here, but set from fd 4 (--args, see[low] Awkward wording in code comment (werkrevisor)
hint
The new Argv doc comment contains the ungrammatical phrase "bwrap's own environment is no way either", which is hard to parse in context. The sentence is trying to explain that setting the environment through bwrap's own variables is also rejected, because bwrap runs on the host and an LD_PRELOAD meant for the sandbox would load into bwrap itself. A phrasing such as "bwrap's own environment is no better" or "setting it through bwrap's environment is no option either" would convey the intent to a future reader.
Fixed in
38550a0: "Setting them as bwrap's own environment is no option either".@@ -127,0 +139,4 @@}var args []bytefor _, e := range environment(home, spec.Env) {if strings.ContainsRune(e.Key, 0) || strings.ContainsRune(e.Value, 0) {[low] Error message ambiguity (werkrevisor)
hint
When EnvArgs refuses an environment variable for containing a NUL byte, the error message reads "environment variable %q holds a NUL byte" and prints only the key name. If the NUL byte is in the value while the key is well-formed, the message attributes the NUL to the key side and misleads the operator. A realistic scenario: a caller runs
-e KEY=val<NUL>tail, gets the error "environment variable "KEY" holds a NUL byte", and looks for the NUL in the key when it is actually in the value.Fixed in
38550a0: the message says "in its name or its value".The sandbox's environment no longer stands on the command line of bwrap (0.13.2)to The sandbox's environment no longer stands on bwrap's command line, nor in a file others can read (0.13.2)🟠 werkrevisor ORANGE for
afdf9ac, superseded by the result for9402adc🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
mixed.Reasons
Findings
Ignored write error at
internal/engine/bwrap.go:401-404, severity unknown, counted as medium, ORANGE, as a line commentContradicts the migration story at
docs/prs/2026-10-20-PR#17-bwrap-env-off-argv.md:22-24, severity unknown, counted as medium, ORANGE, as a line commentEnvArgs called twice at
internal/engine/bwrap.go:112-114, severity unknown, counted as medium, hint, as a line commentClassifier
8 of 9 units passed all questions (1 PR document(s)).
Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 46731, calls 1, tokens in 45108 / out 6310 (reasoning included in out), duration 80.9 s, cost 0.0211 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7369, cost 0.0003 USD, finding judge `jev-1.13.0` calls 3, tokens in 5913, cost 0.0002 USD, build file judge `jev-1.13.0` calls 6, tokens in 8400, cost 0.0004 USD, phases: path_judge 0.5 s, classifier 0.6 s, context 0.3 s, model_review 78.8 s, finding_judge 0.3 s, config: defaults + host + repository (.werkrevisor.toml at main1 answer ignored because of a confidence below [classifier] min_confidence.
03312d4).Reviewed head `afdf9ac1c74438907de8861ba0fb0a2457a2d0f2` against base `03312d484ee61a4a64cec000a474a082ce0b6581`, werkrevisor 0.54.1.
werkrevisor: ORANGE / FINDINGS for
afdf9ac1c74438907de8861ba0fb0a2457a2d0f2.The result comment of werkrevisor holds the details.
@@ -0,0 +19,4 @@bwrap's argv is the last place where they stand.A second leak, found on mih09 the same day: each instance, also of an attached `run --rm`,writes its configuration with the whole environment to `instances/<id>/config.json`, mode 0644, in a directory of mode 0755.Contradicts the migration story, severity unknown, counted as medium (werkrevisor)
ORANGE; Jev: real 0.79, severity 3.0 of 3
Section "For consumers" in CHANGELOG.md tells consumers to "Pass a secret as a bare
-e KEYwith the value in werkdock's own environment, as Werkator does since werkator PR#166". The PR document's Solution section says the secrets end up ininstances/<id>/config.jsonand describes tightening its mode, but never addresses that this very file still serializes the full environment to disk onrun --rmandrun -d. The filesystem-permission change closes the world-readable hole, but config.json still holds every secret in plaintext at rest — a fact the PR document should acknowledge alongside the mode tightening rather than implicitly claiming the secrets are merely off the command line.Fixed in
38550a0: the Non-Goals say that config.json keeps the environment in plain text, why (startof a stopped instance needs it), and that mode 0600 limits it to the account, which holds the secrets anyway.@@ -109,3 +112,1 @@args = append(args, "--clearenv")for _, e := range environment(home, spec.Env) {args = append(args, "--setenv", e.Key, e.Value)if _, err := EnvArgs(spec); err != nil {EnvArgs called twice, severity unknown, counted as medium (werkrevisor)
hint; Jev: real 0.70, severity 1.9 of 3
ArgvcallsEnvArgs(spec)to validate (lines 112-114) and discards the returned byte slice, thenStart(lines 365-367) callsEnvArgs(spec)again to get the actual payload. Iteratingenvironment(home, spec.Env)and checking NUL bytes runs twice per sandbox for no reason. Either haveArgvreturn the payload, or pass through the already-validated slice; otherwise the work is duplicated for every sandbox start.Stays, on purpose:
Argvis also whatwerkdock runvalidates a spec with before it creates the instance (internal/cli/run.go:108), so it must refuse a NUL byte asStartwould. A second pass over a few variables costs microseconds per sandbox start.@@ -362,0 +398,4 @@envR.Close()// written while bwrap reads, since the environment may exceed what// a pipe buffers; bwrap reads it before anything else it doesgo func() {Ignored write error, severity unknown, counted as medium (werkrevisor)
ORANGE; Jev: real 0.81, severity 2.9 of 3
In
Start, the goroutine writes the environment payload to the pipe withenvW.Write(env); envW.Close()and ignores the returned error. While Go's(*os.File).Writeis documented to return a non-nil error when n != len(b) (so a silent truncation on a single call is rare), a write error here signals that bwrap cannot or will not receive the full environment — leaving the sandbox with missing variables and no surfaced failure. With a NUL-framed protocol this is also a correctness issue: a partial write that returns an error drops trailing variables rather than producing a malformed frame, but the supervisor proceeds as if Start succeeded. Loop until all bytes are written or handle the error.Fixed in
38550a0: a failed write kills bwrap before the pipe closes. bwrap reads fd 4 up to EOF before it does anything else, so it never starts a sandbox with part of its environment; Start then fails on the missing child pid.🟠 werkrevisor ORANGE for
9402adc, superseded by the result for1860d07🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
mixed.Reasons
Findings
internal/engine/bwrap.go:403-408, severity unknown, counted as medium, ORANGE, as a line commentClassifier
8 of 9 units passed all questions (1 PR document(s)).
Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 46920, calls 1, tokens in 45286 / out 28445 (reasoning included in out), duration 342.4 s, cost 0.0468 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7419, cost 0.0003 USD, finding judge `jev-1.13.0` calls 1, tokens in 1985, cost 0.0001 USD, build file judge `jev-1.13.0` calls 6, tokens in 8460, cost 0.0004 USD, phases: path_judge 0.4 s, classifier 0.6 s, context 0.3 s, model_review 340.3 s, finding_judge 0.2 s, config: defaults + host + repository (.werkrevisor.toml at main1 answer ignored because of a confidence below [classifier] min_confidence.
03312d4).Reviewed head `9402adc0f93126f6ba6d914ac59932240ebc820e` against base `03312d484ee61a4a64cec000a474a082ce0b6581`, werkrevisor 0.54.1.
werkrevisor: ORANGE / FINDINGS for
9402adc0f93126f6ba6d914ac59932240ebc820e.The result comment of werkrevisor holds the details.
@@ -362,0 +400,4 @@// a pipe buffers; bwrap reads it up to EOF before anything else it// does, so a sandbox that would get only part of it is killed before// the close could hand it that partgo func() {SIGPIPE Risk, severity unknown, counted as medium (werkrevisor)
ORANGE; Jev: real 0.76, severity 3.0 of 3
The goroutine writes the sandbox's environment to
envWwithout ignoring SIGPIPE. If bwrap dies before or while reading fd 4 — a too-large value rejected by execve, an internal bwrap crash, or any external kill of bwrap — the subsequentenvW.Writereturns EPIPE and SIGPIPE terminates the werkdock process before the goroutine can runcmd.Process.Kill(). The go runtime only ignores SIGPIPE fornet.Connreads/writes; rawos.Filewrites do not have this protection. Addsignal.Ignore(syscall.SIGPIPE)(at process startup or around the write) so EPIPE is handled in Go rather than killing werkdock and orphaning bwrap.No change; the finding does not hold for Go. The os/signal documentation, section "SIGPIPE": a write to a broken pipe on file descriptors 1 or 2 makes the program exit with SIGPIPE; on any other file descriptor the runtime takes no action on the signal, and the write fails with EPIPE. That holds for os.File, not only net.Conn. The env pipe is a fresh descriptor, never 1 or 2. Checked with go1.26.10: a program that writes to an os.Pipe whose read end is closed prints "write |1: broken pipe" and exits 0. So the Write returns the error, and the goroutine kills bwrap as intended.
🟡 werkrevisor: YELLOW / HUMAN_REVIEW
Change kind:
mixed.Reasons
Classifier
9 of 9 units passed all questions (1 PR document(s)).
Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 46875, calls 1, tokens in 45244 / out 10306 (reasoning included in out), duration 125.6 s, cost 0.0259 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7419, cost 0.0003 USD, build file judge `jev-1.13.0` calls 6, tokens in 8460, cost 0.0004 USD, phases: path_judge 0.5 s, classifier 0.6 s, context 0.3 s, model_review 123.7 s, config: defaults + host + repository (.werkrevisor.toml at main1 answer ignored because of a confidence below [classifier] min_confidence.
03312d4).Reviewed head `1860d076fbf25f2d48ba4f5785e37144366b4ec1` against base `03312d484ee61a4a64cec000a474a082ce0b6581`, werkrevisor 0.54.1.