fix(unbound-hook): never let stdout encoding abort onboarding setup#153
Merged
Merged
Conversation
| if reconfigure is None: | ||
| continue | ||
| try: | ||
| reconfigure(encoding="utf-8", errors="replace") |
There was a problem hiding this comment.
errors='replace' applied to the hook subcommand's stdout silently replaces unencodable characters (surrogates) with ?. For the diagnostic setup path this is intentional and correct; but the hook path writes structured JSON consumed by Claude Code's hook dispatcher. If a surrogate ever appears in the output, the JSON would be silently corrupted and the hook's allow/block decision would be lost without any indication. Consider using errors='surrogateescape' instead — it round-trips the raw bytes faithfully rather than dropping characters, so the JSON consumer either parses it correctly or fails with a clear decode error.
Jamf's recurring check-in runs the MDM onboarding policy from a launchd context with no LANG/LC_* set, so the interpreter falls back to the ASCII stdout codec. The first diagnostic line setup prints — the migration banner "[migration] python->binary sweep" — contained a U+2192 arrow, which raised UnicodeEncodeError and aborted `setup` (exit 1 -> UNBOUND_INSTALL_FAILED step=setup) on every check-in. Interactive `sudo jamf policy` runs inherit a UTF-8 locale and passed, which masked it during canary. The output is purely diagnostic; a cosmetic write should never kill the install. - main(): reconfigure stdout/stderr to UTF-8 (errors='replace') before dispatching any subcommand, so no print can raise UnicodeEncodeError. Also hardens the `hook` path, whose stdout carries UTF-8 JSON. - setup_cmd: swap the banner arrow for ASCII '->' as defense in depth. - test: drive main(["setup", ...]) with ASCII-backed streams; reproduces the exact crash pre-fix, passes post-fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d889743 to
209a03a
Compare
vigneshsubbiah16
added a commit
that referenced
this pull request
Jun 15, 2026
Lockstep bump of both binaries' __version__ (unbound-hook + discovery) so the release workflow's publish-safety assert passes when tagging runtime-v0.1.5. This is the release that carries the stdout-encoding fix (#153) to the fleet. No behavior change. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vigneshsubbiah16
added a commit
that referenced
this pull request
Jun 15, 2026
* fix(unbound-hook): never let stdout encoding abort onboarding setup (#153) Jamf's recurring check-in runs the MDM onboarding policy from a launchd context with no LANG/LC_* set, so the interpreter falls back to the ASCII stdout codec. The first diagnostic line setup prints — the migration banner "[migration] python->binary sweep" — contained a U+2192 arrow, which raised UnicodeEncodeError and aborted `setup` (exit 1 -> UNBOUND_INSTALL_FAILED step=setup) on every check-in. Interactive `sudo jamf policy` runs inherit a UTF-8 locale and passed, which masked it during canary. The output is purely diagnostic; a cosmetic write should never kill the install. - main(): reconfigure stdout/stderr to UTF-8 (errors='replace') before dispatching any subcommand, so no print can raise UnicodeEncodeError. Also hardens the `hook` path, whose stdout carries UTF-8 JSON. - setup_cmd: swap the banner arrow for ASCII '->' as defense in depth. - test: drive main(["setup", ...]) with ASCII-backed streams; reproduces the exact crash pre-fix, passes post-fix. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(release): bump runtime version 0.1.4 -> 0.1.5 (#154) Lockstep bump of both binaries' __version__ (unbound-hook + discovery) so the release workflow's publish-safety assert passes when tagging runtime-v0.1.5. This is the release that carries the stdout-encoding fix (#153) to the fleet. No behavior change. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
What
unbound-hook setupaborts on Jamf's recurring check-in because the interpreter falls back to the ASCII stdout codec in that launchd context (noLANG/LC_*set). The first diagnostic line setup prints — the migration banner[migration] python->binary sweep— contained a->arrow (U+2192), which raisedUnicodeEncodeErrorand killed the run:Interactive
sudo jamf policyruns inherit a UTF-8 locale and pass — which is why it slipped through canary and only showed up at fleet check-in. The output is purely diagnostic; a cosmetic write must never abort the install.Fix
main()reconfiguresstdout/stderrto UTF-8 (errors='replace') before dispatching any subcommand, so noprintcan raiseUnicodeEncodeError. Also hardens thehookpath, whose stdout carries UTF-8 JSON.setup_cmd: swap the banner arrow for ASCII->as defense in depth.Tests
test_setup_survives_ascii_stdoutdrivesmain(["setup", ...])with ASCII-backed streams — reproduces the exact crash pre-fix, passes post-fix.LC_ALL=C, locale-coercion + UTF-8 mode disabled, piped stdout): pre-fix reproduces the byte-for-byteposition 80crash; post-fix flips the codec to UTF-8 and exits 0.binary/testssetup suite: 16/16 pass (the 4test_hook_clifailures are pre-existing on the base — they need a built discovery binary not present in the checkout).Note
The bash
onboard.sh.tmplalready POSTs install failures to/api/v1/mdm/install-report, but that backend endpoint does not exist yet, so these failures are currently invisible server-side. Tracked separately.Greptile Summary
This PR fixes a
UnicodeEncodeErrorcrash inunbound-hook setupthat aborted onboarding on Jamf fleet check-ins due to Python falling back to the ASCII stdout codec in a launchd context with noLANG/LC_*set.main.py: Adds_force_utf8_io(), called at the top ofmain(), which usesTextIOWrapper.reconfigure()to switchstdout/stderrto UTF-8 witherrors='replace'so noprintcan raiseUnicodeEncodeError.setup_cmd.py: Replaces the→(U+2192) migration banner arrow with ASCII->as defense in depth.test_setup_migration.py: Addstest_setup_survives_ascii_stdout, which reproduces the exact crash scenario (ASCII-backedTextIOWrapperpiped tosys.stdout) and verifies it exits 0 post-fix.Confidence Score: 5/5
Safe to merge — the change is a narrow, well-contained fix for a crash-on-launch bug with no behavioral changes to any non-output code path.
The fix touches only stream reconfiguration at process startup and a one-character banner swap. The core setup logic (settings writes, discovery, key fetch) is untouched. The new regression test accurately reproduces the field crash end-to-end and passes. The reconfigure() call is guarded against both a missing method and runtime exceptions, so the fix cannot itself become a new crash vector.
No files require special attention.
Important Files Changed
Sequence Diagram
sequenceDiagram participant Jamf as Jamf launchd (no LANG/LC_*) participant main as main() participant utf8 as _force_utf8_io() participant stdout as sys.stdout participant setup as setup_cmd.run() Jamf->>main: invoke unbound-hook setup --api-key ... main->>utf8: _force_utf8_io() utf8->>stdout: "reconfigure(encoding="utf-8", errors="replace")" Note over stdout: codec: ASCII → UTF-8 (errors=replace) utf8-->>main: done (or silently skips on ValueError/OSError) main->>setup: setup_cmd.run(rest) setup->>stdout: "print("[migration] python->binary sweep")" Note over stdout: ASCII arrow, no UnicodeEncodeError setup-->>main: "rc=0" main-->>Jamf: exit 0Reviews (2): Last reviewed commit: "fix(unbound-hook): never let stdout enco..." | Re-trigger Greptile