Repository navigation
Conversation
`hack daemon start` and `restart` now bring an installed login service up to the running CLI (HACK-2216). A systemd unit that runs this CLI's executable (same path or a link to the same file) but differs from what it renders is rewritten with the install checks and restarted; a running hackd of another version under that unit is restarted through systemctl. A unit for another existing hack, an unreadable ExecStart, another Hack home, an isolated HACK_HOME and HACK_DAEMON_SERVICE_READ_ONLY keep their refusals. A launchd plist whose program is current but whose content differs is rewritten too. The pure decision lives in planDaemonReconcile (src/daemon/reconcile.ts). `hack update` runs the new executable's `daemon start --json` after a successful install when a service is installed and hackd is running; a failure is reported with `hack daemon install` and never fails the update. `hack daemon start|stop|restart|install --json` print one envelope with data schema hack.daemon-<command>/v1 (manager, action, stale reasons, daemon version/pid); failures use the new E_DAEMON_SERVICE code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@codex review |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
Security findingsAdvisory findings (8)
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 49c0550bbb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
Review round 1 on HACK-2216: - Never rewrite or restart the service from inside it. Linux compares /proc/self/cgroup with the unit's ControlGroup (through the systemctl runner); macOS checks whether the launchd job pid is an ancestor. A stale but compatible daemon is kept (exit 0, blocked inside-service), a mismatched one refused (exit 1); an unreadable check counts as inside. - Keep the service's Hack home: a managed unit whose main file sets HACK_HOME, HOME or an EnvironmentFile that the renderer would drop is not rewritten, and after a rewrite the unit's home is read again before the restart. - launchd: a plist that runs another existing hack (compared by realpath and dev/ino) is never retargeted; only a missing or virtual program is. A mismatched daemon under it is refused with hack daemon install. - hack update: require hackd to answer on this home's socket before reconciling, run the child in reconcile-only mode (HACK_DAEMON_RECONCILE_ONLY=1, which never starts a stopped daemon), and keep observation inside the best-effort boundary. - Owner policy: rewrite on content drift only when the unit starts with Hack's managed header; launchd plists get an equivalent managed marker. Hand-written units keep the HACK-2052 behaviour (report, no rewrite). --json data gains a closed `blocked` field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision on HACK-2216: a launchd plist counts as Hack's when it carries the managed marker or its Label is Hack's (it is only read at Hack's own LaunchAgents path). Plists written by earlier Hack versions, which have no marker, are therefore refreshed on upgrade when they run this CLI's executable, and gain the marker. A plist that runs another existing program stays other-program and is refused. launchd has no drop-ins, so the systemd header rule is unchanged and not applied here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew round 2) - A launchd refresh that fails (unload or load) is an error with E_DAEMON_SERVICE, never 'already running'. - Read the plist's executable as launchd does (Program over ProgramArguments[0]); an unreadable program is 'unknown-program' and never replaced automatically. Absence is established only by no program, a missing path or a virtual path. - macOS restart refuses a service whose program is another existing or unreadable executable before stopping anything; a stopped service under such a plist is not kickstarted (earlier Hack retargeted it instead). - A missing or empty ControlGroup, an empty or malformed /proc/self/cgroup, and a loaded launchd job without a usable pid count as inside the service. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tomatic restart - The automatic reconcile restart now needs the same main-process proof as stop (checkMainProcessServesHome + decideUnitStop), before any change and again just before the restart. Without it, start refuses with blocked unproven-owner and changes nothing. - When an earlier repair wrote the unit but daemon-reload failed (NeedDaemonReload=yes with the restart-pending marker), start finishes the reload and rechecks the unit's home before restarting. A failed reload stops there and keeps the marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on stat errors Review round 3 on HACK-2216: - Parse the plist structurally (src/daemon/launchd-plist.ts): comments are dropped, only keys of the root dict count, and CDATA, unbalanced or unknown structure is unknown. A nested or commented-out Program key no longer makes a foreign top-level program look missing. - A plist program is missing only when stat reports ENOENT or ENOTDIR (classifyLaunchdProgramPath); EACCES, EIO and other errors are unknown and never authorize a retarget. pathExists is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 76681cbb68
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
… restarts and escaped plist keys Review round 4 on HACK-2216: - The final ownership proof before an automatic restart needs a fresh, available state read with no state error; a failed read refuses (blocked unproven-owner) and the earlier snapshot is never reused. - Under HACK_DAEMON_SERVICE_READ_ONLY a pending restart alone no longer restarts a daemon that already runs this version; it stays pending and is reported (blocked read-only). A version mismatch is still restarted. - launchd plist root keys are read exactly as written (no trimming), and a root key with an XML entity or character reference is unknown; program values are not trimmed either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… launchd job ownership Codex findings on HACK-2216: - spawnDetachedDaemon now drops HACK_DAEMON_RECONCILE_ONLY (next to HACK_DAEMON_SERVICE_READ_ONLY) via detachedDaemonEnvironment, and a --foreground start ignores the flag, so a daemon a post-update reconcile replaces actually runs. - Before any automatic launchd refresh or retarget, the plist must not bind another home (HACK_HOME, or a HOME other than the one Hack writes): blocked home-binding. A running job's pid must be proven this home's hackd (proveOwnedDaemonProcess); a running job without one is unproven: blocked unproven-owner. Start refuses to kickstart such a job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f5ebed22e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…oaded Review round 6 on HACK-2216: getLaunchdServiceStatus now records how launchctl print answered (observation: reported, not-found or failed). Only exit 113 with launchd's 'Could not find service' message is not-found. proveLaunchdJobOwnership accepts an unloaded job only when its absence is positively identified, so a failed query refuses the rewrite (blocked unproven-owner) with no bootout, rewrite or bootstrap. loaded keeps its meaning for status and doctor display. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e6d735501d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… a continued line Codex finding on HACK-2216: systemd joins a line ending in a backslash with the next, so a HACK_HOME carried on a continued line escaped the per-line home-binding check and the rewrite dropped it. findDroppedHomeBinding now treats any continued line in the main unit file as a home binding (the simpler, safe option: the rendered unit has none), so the rewrite is refused with blocked home-binding and the unit is left byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
…cement can start Codex finding on HACK-2216: start stopped this home's incompatible direct daemon and only then found the service start refused, leaving no daemon running. - macOS: checkRunningLaunchdDaemon now also refuses up front when the loaded launchd job binds another home (home-binding) or is not proven this home's (unproven-owner). - Linux had the same stop-then-refuse ordering: refuseSystemdStartBeforeReplacing checks what startViaSystemdIfInstalled would refuse (untrusted unit, unreadable unit home, read-only missing program, unvouched loaded files or program) before the stop. Both refuse with exit 1 and leave the running daemon alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…check and the start Review round 9 on HACK-2216: the check before replacing a direct daemon now runs the same planning (planStoppedStart) and non-mutating rewrite computation (computeUnitRewrite) as the start after the stop. A missing program with no durable replacement, an unrenderable program, or a program or service PATH that fails install's checks is refused before the stop (blocked rewrite-refused). The retarget of a missing program now also runs install's program and PATH checks. Runtime failures and concurrent changes between the check and the start remain outside the guarantee (documented). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ck to a direct start Review round 10 on HACK-2216 (owner decision: make the guarantee structural): - The pre-stop check now also applies the effective-configuration checks the start runs after a rewrite (loaded files, effective PATH and service programs, via checkSystemdEffectiveService/checkSystemdEffectivePath) to the configuration that survives it, so an unsafe PATH from a retained drop-in is refused before the stop (rewrite-refused). - When a running daemon was replaced and the service then cannot start the replacement for any reason, start starts this home's hackd directly (startReplacementDaemon), reporting action restarted, fallback direct, the service's blocked reason and a hack daemon install warning. If the direct start fails too, the service error stands. --json data gains a closed fallback field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… explicit start outcome Review round 11 on HACK-2216: - The fallback runs only on positive evidence that the manager will not start hackd: systemd inactive or failed, not auto-restart, no queued job (or the unit not loaded); launchd job not loaded, or loaded, stopped and asking for no retry. Pending or unknown returns the service error with blocked service-pending. - A rejected service start is treated like a failed one behind the same gate; if the fallback is not used or fails, the original error is rethrown. - The service start reports an explicit outcome (service-refused, service-attempted or direct-attempted, from onStartRequested hooks at the systemd start and the launchd reload/kickstart), and the fallback decides from it instead of the --json trace, so an early systemd refusal with an empty trace still falls back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3a49b04 picked up .pr-body.md, a local copy of the PR description used to edit it. It holds only the public PR text; it does not belong in the repository. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 91cc203cd4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: 91cc203cd4
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
…t's version When a newer CLI ran by absolute path while an older hack came first on PATH, a replacement's direct fallback spawned the PATH-first hack, and the start counted as successful on pid and socket alone, so the post-update reconcile reported an upgrade that did not happen. - A replacement's direct start (startReplacementDaemon's fallback, and the direct start after a replaced daemon in startStoppedDaemon) runs executingDaemonArgv: the executing compiled binary, or the same runtime and entry point for a source invocation, never the PATH-first hack. Ordinary starts with nothing to replace are unchanged. - startReplacementDaemon now requires verifyStarted: after any replacement start, through the service or directly, the daemon must answer on this home's socket and report this CLI's version (probeDaemonApi compatibility). Otherwise exit 1 with action none (E_DAEMON_SERVICE), never restarted, so the update hook records daemon.status failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h and the published file The post-update hook re-executed binaryPath after publishRelease. With an install directory another account can write (a custom HACK_INSTALL_BIN), a payload could be renamed over it before the spawn and run as the updating user. - publishRelease returns installedBinary: the device and inode of the file it published (the rename keeps the staged inode) and the sha256 it already records in the backup manifest. update.ts passes it to reconcileDaemonAfterUpdate. - verifyInstalledBinary runs right before the spawn: checkServiceExecutableTrusted on the path (no directory on the way replaceable by another account), no link at the final component (lstat regular file), and the same device, inode and sha256. Any failure runs nothing and records daemon.status failed with a hack daemon start next step; the update itself still succeeds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: c0f95fcb37
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
runLaunchctl spawned bare launchctl through the inherited PATH, so a planted launchctl in an earlier PATH directory ran as the user after hack update, which this PR makes reach launchd automatically. - runLaunchctl, the only place the launchd module runs launchctl, now runs /bin/launchctl (LAUNCHCTL_PATH, launchctlArgv), so every launchd operation is covered. The macOS ancestry check already ran /bin/ps; it is now named (PS_PATH, psParentArgv) and pinned by a test. - Test seam: useLaunchctlBinaryForTests, called only by the test preload tests/helpers/launchctl-stub-preload.ts from HACK_TEST_STUB_LAUNCHCTL. No production code reads an environment variable for the binary. - Other spawns in files this PR touches: systemctl, loginctl and getent go through the systemd runner, which resolves them on PATH but refuses one whose path another account could replace (unchanged); the replacement spawns (executingDaemonArgv) and the post-update re-exec run absolute, verified paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…proofs An automatic replacement reached bare ps (the orphan sweep's process table, and the ownership proof's process births) and a PATH-resolved lsof (socket owners), so a planted ps or lsof in an earlier PATH directory ran as the user. - resolveSystemTool (src/lib/system-tool.ts) returns a fixed absolute path, never PATH: /bin/ps and /usr/sbin/lsof on macOS; on Linux the first existing of /bin/ps or /usr/bin/ps, and /usr/bin/lsof or /usr/sbin/lsof, accepted only if checkReplaceableChain passes (the chain check checkServiceExecutableTrusted runs). None qualifying is null. - process.ts (listProcessTable, listOpenUnixSockets, the socket-owner probe) and host-command-observation.ts (readObservedProcesses) use it. No trusted ps makes the orphan scan unknown, and the sweep then stops nothing and starts nothing; no trusted lsof leaves socket owners unknown; no trusted ps leaves a process snapshot unknown. - Test seam useSystemToolForTests, test-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: 7f489a6f93
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
…ackd relationship directories On the automatic paths checkSystemdUnitFile chmod-ed a writable user-unit directory before the load-path check, and no check looked at hackd.service.wants/.requires/.upholds. Another account that could write the directory could pre-plant payload.service plus a .wants link; the directory then looked trusted, daemon-reload loaded both and the restart ran the payload. - checkOwnerControlledPath repairs an owned directory only with repair: true. Only explicit hack daemon install passes it (securing its own directories and the checks right after); start, stop, restart, the pre-stop check and the automatic gate refuse a group- or other-writable directory with untrusted-unit and change nothing. On next, these checks repaired on every path. - checkUnitLoadDirectory now checks hackd.service.wants/, .requires/ and .upholds/ in every user-unit load directory: each must be absent, or an empty directory owned by this user or root that others cannot write. A link, any entry or a writable one is refused. Hack's renderer and install create none of them. This runs before every reload, start and restart (and in stop and install). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision: a stop loads and runs no new unit code, and blocking it would keep the user from shutting hackd down exactly when something looks wrong. checkSystemdStopHooks no longer runs checkSystemdUnitTrusted plus checkSystemdUnitLoadPaths, which now refuse writable unit and load directories and hackd.service.wants/.requires/.upholds. Instead it checks the unit file itself (a regular file this user owns that others cannot write; directories neither judged nor repaired), then, as before, what systemd has loaded (fragment and loaded drop-ins) and the absence of ExecStop hooks. Stop's ownership and home checks are unchanged, and stop never reloads. Start, restart (which runs the full checks before this one), the pre-stop check, the post-update reconcile, the automatic gate and install keep the full refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nstall Automatic rewrites (rewriteUnitForPlan into installSystemdUnit) still passed repair: true inside the shared installer, so a directory that became writable after the earlier gate was chmod-ed closed and the run went on with whatever was planted in that window. - installSystemdUnit takes repair, defaulting to false, and threads it into ensureOwnerWritableOnlyDirectories, the drop-in check and the unit-file check. Without it, a writable unit directory or a replaceable unit file is refused (untrusted), with the mode unchanged and nothing written, reloaded or restarted. - Only explicit hack daemon install (installDaemonSystemdService) passes repair: true. It is reached from the install command and from doctor --fix after an explicit confirmation; no automatic path calls it. - rewriteUnitForPlan records blocked untrusted-unit when the installer refuses for trust. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
|
Ready for lead: What changed since
What I ran
Decisions to confirm
Mac pass: how to run it without touching the owner's login serviceslaunchd cannot be isolated on the owner's macOS account. The label is the fixed
Not verified anywhere: real launchd, real I will not push to this branch after this note. |
…econcile Brings in next at 081a526 (#811 supervisor opt-in runner, #863 candidate launcher names HACK-2251, #808 desktop Logs tab). No conflicts. The CLI reference, machine-output table and error-code reference regenerate unchanged; E_DAEMON_SERVICE kept. #863 changes which launcher printed commands name and the candidate update refusal; it does not change the executing-CLI identity, the service program checks, durability or the post-update binding, and a candidate install is still refused before any install or reconcile. #811's supervisor reads process identities through process-tree.ts and runs its own sibling binary; it does not use this PR's ps/lsof helpers, process-ancestry or the orphan sweep. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 417f7418bf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
…icit model gap AGENTS.md asks release-critical lifecycle changes to keep docs/reference/v5-model-coverage.md current. Owner decision: docs only, no new TLA+ model. The new row names the invariants the reconcile relies on, the source transitions and regression tests that cover them, and what stays unqualified: no finite model or bound, one reconcile at a time per home, crashes between stop and start, power loss, real systemd and launchd, real launchctl print parsing, concurrent manager restarts, and the pending Mac launchd pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The coverage row said "Coverage gap: none" and then that no model covers it. It now reads "Coverage gap: no maintained model; not model-checked." Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: 2da1ec10a2
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
Behavior
hack daemon startandhack daemon restartnow bring an installed hackd login service up to the running CLI, so most upgrades no longer need a manualhack daemon install.systemd (Linux), version mismatch: when the unit runs this CLI's executable (the same path, or a path that resolves to the same file by realpath or device/inode) and a running hackd reports another version, start restarts the service with
systemctl --user restart. It first makes the loaded-file, program and stop-hook checks thatstopmakes.systemd, stale content: start rewrites a stale unit only when all three hold:
# Managed by hack: written by \hack daemon install`...`), the owner policy;HACK_HOMEorHOMEinEnvironment=, or anyEnvironmentFile=).The rewrite keeps the program path as written, so a symlink stays a symlink, and keeps the unit enabled or disabled as it was. It uses
install's program,PATHand unit-directory checks. When hackd is running, the unit's effective home is read again after thedaemon-reloadand before the restart.Hand-written units: a unit without the header keeps the HACK-2052 behaviour. Start reports the drift with
hack daemon installas the next step and does not rewrite it.Never from inside the service: before any rewrite or restart of a running service, start checks whether it runs inside that service.
/proc/self/cgroupwith the unit'sControlGroup, read through the fakeable runner.none,blocked: "inside-service") and a mismatched daemon is refused (exit 1). Nothing is stopped. If the check cannot be made, the caller counts as inside.Refusals kept: a unit for another existing
hack, an unreadableExecStart, another Hack home, an isolatedHACK_HOMEwithout the opt-in,HACK_SYSTEMD_DISABLE, andHACK_DAEMON_SERVICE_READ_ONLY(which never writes or reloads).launchd (macOS):
Programwins overProgramArguments[0]. A plist whose program cannot be read (not a property list, a repeated key, a non-string value, an XML escape, a relative path) is"unknown-program"and never replaced automatically./$bunfs).E_DAEMON_SERVICE), never "already running".hack daemon restartrefuses before stopping anything. Earlier Hack retargeted such a plist instead; the next step ishack daemon install.hackis left alone ("other-program"), and a mismatched daemon under it is refused withhack daemon install.Labelis Hack's (dance.hack.hackd), so plists written by earlier Hack versions (no marker) are refreshed on upgrade. It is only ever read at Hack's own LaunchAgents path. The older unbounded-output repair is kept. A plist with neither the marker nor Hack's label is reported as"unmanaged". launchd has no drop-ins, so the systemd header rule does not apply.hack update: after a successful install, the update checks that a login service is installed and that this home's hackd answers on its socket. A live pid alone does not count. It then runs the new executable'shack daemon start --jsonwithHACK_DAEMON_RECONCILE_ONLY=1. In that mode, start returns actionnonewhen no hackd of this home runs, so a daemon that stopped in the meantime is never started. The step is best effort: observation, run and parsing all report failures in the update result asdaemon(withnext_step: "hack daemon install") and never fail the update.Shared logic: the pure decision is
planDaemonReconcileinsrc/daemon/reconcile.ts. Its inputs areunit,programIsOurs,readOnly,running,restartPending,contentBlock,invokerInsideServiceandcliVersion; it returnsrewrite,service,reasonsandblocked.JSON contract (
--jsonondaemon start|stop|restart|install)Only the envelope goes to stdout; human lines go to stderr. Exit codes are unchanged, and
start --foreground --jsonisE_USAGE.These commands follow the shared envelope rule from #835 (
docs/reference/machine-readable-output.md): exactlyokanddataat the top level, with the versioned schema insidedata. They areschema-envelopein the JSON command inventory (src/cli/json-contracts.ts), and their schemashack.daemon-start.v1,hack.daemon-stop.v1,hack.daemon-restart.v1andhack.daemon-install.v1are registered inMACHINE_SCHEMAS.{ "ok": true, "data": { "schema": "hack.daemon-start.v1", "manager": "systemd", "action": "reinstalled", "stale": true, "stale_reasons": ["content"], "blocked": null, "cli_version": "4.3.0", "daemon": { "running": true, "pid": 1234, "version": "4.3.0", "compatible": true } } }manager:systemd | launchd | noneaction:none | started | restarted | reinstalled | stopped | installedstale_reasons:content | program-missing | version-mismatchblocked:null | read-only | other-program | unknown-program | unmanaged | home-binding | inside-service | unproven-owner | rewrite-refused | service-pending | untrusted-unitfallback:null | direct(a replaced daemon was started directly because the service could not start it){ "ok": false, "error": { "code": "E_DAEMON_SERVICE", "message": <last warning/error printed>, "detail": <same data> } }. Access refusals keepE_PERMISSION.E_DAEMON_SERVICEis a new registry code, and the generatedhack-diagnosereference is regenerated.Merge of next at 081a526 (merge commit 417f741)
E_DAEMON_SERVICEis kept.src/lib/candidate-launcher.ts) and the candidate update refusal. It doesn't touchresolveExecutingCliPath,executingDaemonArgv,resolveHackInvocation,isSameExecutableFile, the launchd and systemd program checks, durability, or the post-updateinstalledBinarybinding.process.execPath(itsbundle/hack-cli).HACK_CANDIDATE_ENTRYPOINTcarries over.src/lib/process-tree.tsand runs its own siblinghack-lifecycle-supervisorbinary. It doesn't use this PR'sresolveSystemTool,listProcessTable,readObservedProcesses,process-ancestryor the orphan sweep.hack host psstill takes its snapshot fromreadObservedProcesses(now a fixed, trustedps); feat(lifecycle): opt into built-in host process supervision #811 only extends how that snapshot is grouped.Review round 37 fix: repair only from explicit install (commit 2f26965)
rewriteUnitForPlanintoinstallSystemdUnit) still passedrepair: trueinside the shared installer. A directory that became writable after the earlier gate was chmod-ed closed, and the run went on with whatever was planted in that window.installSystemdUnit(src/daemon/systemd.ts) takesrepair, defaulting to false. It threads it intoensureOwnerWritableOnlyDirectories, the drop-in check and the unit-file check.untrusted.rewriteUnitForPlanrecordsblocked: "untrusted-unit"when the installer refuses for trust.repair: trueinsrc: the only caller is explicit install,installDaemonSystemdService(src/commands/daemon-systemd.ts). It is reached fromhack daemon install, and fromhack doctor --fixonly after the user explicitly confirms the install.src/commands/global.tsoffershack daemon installfor launchd on macOS, which does not use the systemd installer.src/commands/project.tshas an unrelatedrepairoption.repair: true.untrusted-unit, mode still0775, the stale unit unchanged, nodaemon-reloador restart; explicit install then repairs it;installSystemdUnitwithoutrepairrefuses a writable unit directory with nothing written or reloaded; withrepairit secures and writes.Both fail with repair forced on.
Owner decision:
hack daemon stopis not blocked by those checks (commit 6b3a292)checkSystemdStopHooks(src/daemon/systemd.ts) no longer runscheckSystemdUnitTrustedpluscheckSystemdUnitLoadPaths. Those now refuse writable unit and load directories andhackd.service.wants/.requires/.upholds.checkUnitFileItself): a regular file this user owns that others cannot write; directories are neither judged nor repaired;checkLoadedUnitFiles), and noExecStoporExecStopPosthooks.decideUnitStop, main-process proof, fresh re-read before the stop), and stop never runsdaemon-reload.checkSystemdStopHooks, but it runs the fullcheckSystemdUnitTrustedandcheckSystemdLoadedUnitPathsjust before it.hackd.service.wants/payload.servicelink,stopstops this home's proven daemon (systemctl --user stop, nodaemon-reload), while the automatic restart and a later start refuse;stopstops it without a reload, and the directory's mode is unchanged (0775).Both fail with the old broad stop check restored.
Codex security finding: no automatic repair, and relationship directories (commit 2088b96)
checkSystemdUnitFilechmod-ed (repaired) a writable user-unit directory before the load-path check ran. No check looked athackd.service.wants/,.requires/or.upholds/.payload.serviceplus a.wantslink.daemon-reloadloaded both units, and the restart ran the payload.nextdoes today: these checks (checkSystemdUnitFile,checkSystemdUnitDropIns, throughcheckOwnerWritableOnlyDirectory) removed group and other write from owned directories on every path that used them: install, start and stop.checkOwnerControlledPathrepairs an owned directory only withrepair: true.hack daemon installpasses it: securing its own directories, and the drop-in and unit-file checks right after.blocked: "untrusted-unit", and leave its mode unchanged.checkUnitLoadDirectorynow checkshackd.service.wants/,.requires/and.upholds/in every user-unit load directory.installcreate none of them (systemctl enablelinks intodefault.target.wants/).checkSystemdUnitLoadPaths, so before every reload, start and restart (the automatic gate,checkSystemdLoadedUnitPaths), and instopandinstall.tests/daemon-systemd.test.ts):0775, with no reload or restart;hackd.service.wants/payload.servicelink stops the automatic restart, the pre-stop check and a stopped start, with no reload, start or restart;.requiresand.upholdsare refused the same way;The existing start test now asserts the refusal and unchanged mode, then that install repairs it and start proceeds. The drop-in mask test asserts the refusal without repair, and the repair with
repair: true. Each test fails with its fix disabled.Review round 35 fix: ps and lsof in the process proofs (commit 7f489a6)
ps, and the socket-owner probe foundlsofon PATH.resolveSystemTool(src/lib/system-tool.ts) returns a fixed absolute path and never consults PATH:/bin/ps,/usr/sbin/lsof;/bin/psor/usr/bin/ps, and of/usr/bin/lsofor/usr/sbin/lsof, accepted only ifcheckReplaceableChainpasses (the chain checkcheckServiceExecutableTrustedruns);listProcessTable(orphan sweep): no trustedpsmakes the scanunknown.sweepOrphanDaemonsthen stops nothing, and the start does not go on as if no orphan held the socket;listOpenUnixSocketsand the macOS socket-owner probe: no trustedlsofleaves socket owners unknown (null), so ownership is not proven;readObservedProcesses(ownership-proof births on macOS, and its non-daemon callers): no trustedpsleaves the snapshot unknown (null), as a failedpsalready did.findOrphanDaemonProcesses→sweepOrphanDaemons(start, restart, replacements) anddefaultDaemonProcessProbe(birth,socketOwners), used byproveOwnedDaemonProcessfor stop, the replacement and the restart ownership proofs;readObservedProcesses:src/commands/host-ps.ts(hack host ps),src/commands/usage.ts(hack usage), and the host-command observation in the same module. They now use the fixed path too; their suites pass.useSystemToolForTests, test-only. No existing test fakes these functions'psorlsofthrough PATH. The two tests that plant apsstub exercise other code, and their suites pass unchanged.psuses elsewhere in Hack (compose, crash capture, lifecycle) are outside this PR and these daemon paths.tests/system-tool.test.ts):psandlsofstubs planted first on PATH, the orphan sweep, a process snapshot, and the ownership probe as macOS runs it (births throughps, socket owners throughlsof) spawn only absolute paths outside the planted directory, and the planted stubs never run;resolveSystemToolpicks the fixed candidates in order, skips an untrusted one, and returns null when none qualifies;unknown.The main test fails with a bare
psput back in either the sweep or the snapshot read.Codex security finding: launchctl and ps by absolute path (commit 3e6461c)
runLaunchctlspawned barelaunchctlthrough the inherited PATH. A plantedlaunchctlin an earlier PATH directory would run as the user afterhack update. That was already true onnext; this PR makes it reachable automatically.runLaunchctlis the only place the launchd module runslaunchctl. It now runs/bin/launchctl(LAUNCHCTL_PATH,launchctlArgvinsrc/daemon/launchd.ts), so every launchd operation is covered./bin/ps. It is now named (PS_PATH,psParentArgvinsrc/daemon/process-ancestry.ts) and pinned by a test.useLaunchctlBinaryForTests. It is called only by the test preloadtests/helpers/launchctl-stub-preload.ts, which readsHACK_TEST_STUB_LAUNCHCTL. No production code reads an environment variable for the binary. The launchd driver tests load it after the darwin preload.runLaunchctl, which is the fix itself.launchctl: changed, as above;ps: already absolute;systemctl,loginctlandgetentgo through the systemd runner. It resolves them on PATH but refuses one whose path another account could replace (checkReplaceableChain, HACK-2052). Unchanged;executingDaemonArgv) and the post-update re-exec already run absolute, verified paths.launchctl setenvinsrc/commands/global.ts(TLS trust) still runs by name, but this PR does not touch that file or its path.launchctlshim no longer sees the launchd module's calls. For those,HACK_LAUNCHD_DISABLE=1, which is checked before any spawn, is the guard.launchctlArgvstarts with/bin/launchctl;psParentArgvstarts with/bin/ps.Codex findings: replacement spawn and post-update re-exec (commits 81c4372, c0f95fc)
Replacement spawn (81c4372). When a newer CLI ran by absolute path while an older
hackcame first on PATH, a replacement's direct fallback spawned the PATH-firsthack. The start counted as successful on pid and socket alone, so the post-update reconcile reported an upgrade that had not happened.executingDaemonArgv(src/daemon/executing-cli.ts): the executing compiled binary, or the same runtime and entry point for a source invocation. It never runs the PATH-firsthack.startReplacementDaemon's fallback and the direct start instartStoppedDaemonafter a replaced daemon: thestartreplacement,restartand the reconcile-only replacement.startReplacementDaemonnow requiresverifyStarted. After any replacement start, through the service or directly, the daemon must answer on this home's socket and report this CLI's version (probeDaemonApicompatibility,describeUnverifiedReplacement).none,E_DAEMON_SERVICE, neverrestarted. The update hook then recordsdaemon.status: "failed".hack;Post-update re-exec (c0f95fc). The post-update hook re-executed
binaryPathafterpublishRelease. With an install directory another account can write (a customHACK_INSTALL_BIN), a payload could be renamed over it before the spawn.publishReleasereturnsinstalledBinary: the device and inode of the published file (the rename keeps the staged inode) and the sha256 it already records in the backup manifest.update.tspasses it to the hook.verifyInstalledBinary(src/commands/daemon-upgrade.ts) runs right before the spawn. It requires:checkServiceExecutableTrustedon the path;Any failure runs nothing and records
daemon.status: "failed"withhack daemon startas the next step. The update itself still succeeds./dev/fd. The trust check closes the window between the identity check and the spawn for other accounts.publishRelease's returned identity matches the published file.The hook tests fail with the check disabled.
Review round 32 fix (commits 7ffaf75, 3a49b04, 91cc203)
checkLaunchdPlistTrusted(src/daemon/launchd.ts) requires:~/Library/LaunchAgentsdoes not let others add or replace entries;checkReplaceableChain).Nothing is repaired. It runs before any unload, write or bootstrap: install (explicit, implicit autostart, refresh, retarget), the reload before a kickstart, and the check before a running daemon is stopped. A failure is
blocked: "untrusted-unit"(repair resultuntrusted-plist), consistent with the documented meaning ofuntrusted-unit.O_NOFOLLOWdescriptor and requires exactly the checked bytes.launchctl bootstrap.writeTextFileAtomicIfChangedwithnoFollowTarget, which refuses a link there) and binds its bootstrap to those bytes the same way. The reload path is bound as above.bootout) is neither bootstrapped nor kickstarted; a positive control shows the same scenario otherwise reachesbootoutandbootstrap;~/Library/LaunchAgents, is refused before any unload;~/Library/LaunchAgentsloads nothing.The swap and trust tests fail with the binding or the trust check disabled.
/nonexistent-home.Review round 31 fixes (commit 13a4809)
PATH. launchd's default or inherited PATH is never assumed, and the default-PATH fallback is gone.loadedJobMatchesPlistrequires that same explicitPATHin the loaded job's own environment, so a job without its own PATH never matches.PATHand the exact command: the first launchd renderer already did. A Hack plist without them is refreshed by the normal checked repair, which writes both. Any other such plist is refused withrewrite-refusedand thehack daemon installstep.checkLaunchdJobBeforeStartnow validates the plist's completeProgramArgumentsas exactly<program> daemon start --foreground, withProgramtaking precedence as launchd does.RunAtLoadorKeepAlivecannot run custom arguments. The newparseLaunchdPlistCommandreads the full argument vector.isRunningcallback. A job that started running meanwhile is not booted out, and an unreadable status counts as running.docs/architecture.mddocuments reallaunchctl printparsing as an unverified compatibility point, with this as the fallback.Programthat differs from the first argument: each refused before anything changes, with start unloading, reloading and kickstarting nothing;The argument, PATH and fresh-running checks each fail their tests when disabled.
Review round 30 fixes (commit 6792a61)
src/daemon/launchd-print.ts(parseLaunchdPrintJob) reads the loaded job strictly fromlaunchctl print gui/<uid>/dance.hack.hackd:program, theargumentslist and the job's ownenvironmentPATH (not the inherited or default environment). Anything malformed, repeated or unbalanced isunknown.verifyLaunchdJobBeforeKickstart(src/daemon/launchd.ts) runs before every automatic kickstart: start, restart, the post-update reconcile, a stopped-service start and autostart. It checks the plist on disk, then requires the loaded job to run exactly that plist: the same program, Hack's arguments and the same PATH.rewrite-refused, because a reload would stop hackd. An unverified loaded configuration is never kickstarted.installLaunchdServicenow runscheckServiceExecutableTrustedon the program it writes before any unload, write or bootstrap (unsafeProgram). Every caller shares it: explicit install, the implicit autostart install, refresh and retarget.checkLaunchdRetargetTarget.tests/launchd-print.test.ts): real-lookinglaunchctl printoutput with inherited, default and job environments and nested event triggers; the strict refusals;hackin a group-writable directory: no bootstrap.Each fails with its fix disabled.
launchctl printnow emits a service block. The autostart positive control has a matching loaded job, and expects the extraprintthat reads it.Merge of next at 34b5053 (merge commit 240dd08)
src/lib/machine-output.tsandsrc/cli/json-contracts.tsconflicted: both sides added schemas and variants. Kept feat(update): report available updates read-only for any install kind (HACK-2218 R14) #860'supdateCheck/update --check --observeand this PR's four daemon command schemas.src/commands/update.tsconflicted. Kept both feat(update): report available updates read-only for any install kind (HACK-2218 R14) #860'sobserveUpdate/handleObserveand this PR's post-update reconcile.--observereturns at the top of the handler, and plain--checkreturns before the install.reconcileDaemonAfterUpdateruns only after a real install. feat(update): report available updates read-only for any install kind (HACK-2218 R14) #860'stests/update-observe.test.tsruns the real CLI with--check --json --observeand asserts that nothing is spawned, which would fail if the reconcile ran.update --check --observerows) and the error-code reference (E_DAEMON_SERVICEkept).docs/reference/v5-model-coverage.mdis hand-written, this PR does not touch it, and it is identical to next's.Codex security finding: check the program and PATH before every automatic start (commit 8610528)
dockerthrough a PATH directory another account could write.checkLaunchdJobBeforeStart(src/daemon/launchd.ts) checks the plist launchd will start:checkServiceExecutableTrusted;PATH, or launchd's default when it sets none), withcheckServicePathDirectories;start,restart, the post-update reconcile and a stopped-service start, throughstartViaLaunchdIfLoaded;ensureLaunchdDaemonReady), which then falls back tohack daemon startand gets the same refusal.A refusal kickstarts nothing, with
blocked: "rewrite-refused"and thehack daemon installstep.start-refused, reported asrewrite-refused), so nothing is stopped for a start that would be refused.checkSystemdLoadedUnitPaths(loaded programs and effective PATH) right beforesystemctl start;refuseUntrustedProgramLast) now covers the effective PATH too.hack daemon start;Each fails with its check disabled.
Codex security finding: check the launchd PATH (commit 7b3fe8a)
hackexecutable. hackd runs helpers such asdockerby bare name through the plist's PATH, so a PATH directory another account can write gave code execution as this user.installLaunchdService(src/daemon/launchd.ts) now checks every directory of the PATH the rendered plist gives hackd before it unloads or writes anything. Those directories are/usr/local/bin,/opt/homebrew/bin,/usr/bin,/bin,~/.bun/binand~/.hack/bin. The check ischeckLaunchdServicePath, which uses systemd'scheckServicePathDirectorieswith no administrator-directory exception. A failure is reported asunsafePath.hack daemon installon macOS had no PATH check. It now refuses an unsafe PATH the same way, so explicit install and the automatic rewrite agree.refresh-refusedorretarget-refused(blocked: "rewrite-refused",hack daemon installstep). Nothing is written, unloaded or loaded.rewriteTargetIsFit) makes the program and PATH checks for a refresh or retarget the start would run.renderedLaunchdPathDirectories). The system directories have a test-only in-process seam, as systemd's do (HACK-2142). The test driver points it at a fixture directory, so CI runner images with writable system directories don't decide these tests.refresh-refused, no bootout or bootstrap, and an unchanged plist;~/.hack/binis world-writable givesretarget-refused, with nothing changed;unsafePathand no bootstrap.The three refusal tests fail with the check disabled.
Review round 27 fix (commit 080a785)
refuseUntrustedProgramLast(src/commands/daemon-systemd.ts) now reads the single loadedExecStart(readSystemdLoadedProgram). It runscheckServiceExecutableTrustedon that exact pathname, symlinks and every directory above it included, as well as on the main file's program.blocked: "unproven-owner". An untrusted one givesrewrite-refused. Either way nothing is restarted.ExecStartentries at the last check are not restarted (unproven-owner).Both fail with the fix disabled.
Codex security finding: check the kept program before every automatic rewrite (commit 26d9fbd)
isSameExecutableFileaccepted equal path strings. Someone able to replace that pathname would have launchd boot out the safe daemon and bootstrap their program as this user.repairLaunchdProgramIfInvalid(src/daemon/launchd.ts) now runscheckLaunchdRetargetTargeton the program the rewritten plist will run, for a refresh and a retarget alike, as its last step before the write, bootout and bootstrap. That check rejects a source file or bun script, requires a durable location, and runscheckServiceExecutableTrustedalong the whole path.refresh-refused, reported asblocked: "rewrite-refused"with thehack daemon installstep. Nothing is written, unloaded or loaded.refreshBlocksReplacement) refuses a refresh the start would run for an unfit program, so nothing is stopped.isSameExecutableFilealways resolves both paths and compares device and inode. Equal strings never short-circuit.checkServiceExecutableTrustedplus the service PATH) insiderewriteUnitForPlan, right beforeinstallSystemdUnit. An automatic restart now checks the unit's program once more as its very last step, after the ownership proof and loaded-program read (refuseUntrustedProgramLast,blocked: "rewrite-refused").refresh-refused, no bootout or bootstrap, and an unchanged plist;rewrite-refused.Each fails with its fix disabled. The launchd fixture's temp-dir
hacknow counts as durable by default through the existing driver seam; the temporary-location test opts out.Merge of next at 345dc0d, with the #835 envelope rule (merge commit af7957d)
plugins/hack/skills/hack-diagnose/references/error-codes.md(content revision line). It was regenerated withbun run generate:agent-plugins: next's rows plusE_DAEMON_SERVICE.okanddata, the schema insidedata, and{ok:false,error:{code,message,detail}}on failure. The four commands are recorded asschema-envelope, and their schemas are added toMACHINE_SCHEMASandSCHEMA_JSON_VARIANTS.hack.daemon-<command>/v1to the registry's dotted formhack.daemon-<command>.v1.buildDaemonCommandDatatakes it from the registry (DAEMON_COMMAND_SCHEMAS), and the post-update parser checks the same constant. Fields, enums, error code and exit codes are unchanged.docs/reference/machine-readable-output.md, which now has four daemon rows. The CLI reference had no diff.docs/architecture.mdlinks the shared rule, and the command contracts use the new ids.Review round 24 fix (commit effa1dc)
startViaSystemdIfInstalledrefused an untrusted main unit (another account's, writable by others, or a link) before reaching the trust gate, and did not record why, sodaemon start --jsonreportedblocked: null. Both refusals now recordmanager: "systemd",action: "none"andblocked: "untrusted-unit"through one helper (recordUntrustedUnit). The early message ends with thehack daemon installstep.manager: systemd,action: none,blocked: untrusted-unit, no systemctl mutations, and the file unchanged. It fails without the fix. The round-11 test that pinned an empty trace for this refusal now expects the recorded reason, and still checks that systemd was not asked to start.Codex security finding: unit trust before any change (commit 01b83fd)
Wants=of its own unit), and the restart then ran it.checkSystemdUnitTrustedBeforeChange(src/daemon/systemd.ts) runs the checks install and enable use:checkSystemdUnitTrusted(unit file owner, mode and no link, the directories above it, drop-in directories), every user-unit load directory, and the loaded fragment and drop-ins that a rewrite keeps. It does not judge the loaded program, because a retarget replaces a missing one and the check just before the restart judges what is loaded then.refuseSystemdStartBeforeReplacing, so a running daemon is never stopped for an untrusted unit.untrusted-unitandhack daemon installas the next step. The reason is added to the enum,docs/architecture.mdand the command contracts. The preflight's existing unit-file trust refusal now reportsuntrusted-unitinstead ofunproven-owner.tests/daemon-systemd.test.ts), each with a missing program and a plantedWants=:trustUidseam): no write, no systemctl mutation, no marker,untrusted-unit; the trusted control retargets and reloads;untrusted-unit;untrusted-unitwith the install step, no mutations.The CLI test for a world-writable unit under
daemon restartnow expectsuntrusted-unit; it still checks that the running daemon is not stopped.CI fix: spawn env isolation (commit 62ae3e4)
bun buildwithout an env, so it inherited the runner's environment and HOME. It now spawns withprivateCliEnv({ root }), sotests/cli-spawn-env-isolation.test.tspasses without an allowlist entry.Review round 19 fix (commit 356ac41)
launchdJobBlocksReplacement(src/commands/daemon-launchd.ts) is the pre-stop check shared by start'scheckRunningLaunchdDaemonand restart'srefuseLaunchdBeforeReplacing. When the plist's program is missing or virtual, it now applies the retarget that would follow the stop, using the same helpers as the repair:checkLaunchdRetargetTarget, or it refuses asrewrite-refused;refuseRunningRewrite, now exported fromsrc/daemon/launchd.ts):unproven-ownerfrom source,other-programfor another binary.hack daemon installas the next step.Review round 17 fixes (commit 9b4b830)
Settled policy: a source invocation may repair a missing program on a stopped service, but nothing may unload or restart a running daemon unless the executing CLI is eligible.
repairLaunchdProgramIfInvalid(src/daemon/launchd.ts) runsrefuseRunningRewritewhenever hackd runs. A running daemon is one thatstartViaLaunchdIfLoadedfound running and compatible (daemonRunning), or a job launchd reports running.unproven-ownerfrom source,other-programfor another binary, withhack daemon installas the next step.checkLaunchdRetargetTargetapplies the systemd retarget checks..ts,.js...) and a#!script run by bun.assessExecutableDurability) and trusted (checkServiceExecutableTrusted).retarget-refused, and start reportsblocked: "rewrite-refused". The retarget now writes the validated target explicitly.Codex finding: "this CLI" is the executing binary (commit d28eddb)
hackon PATH.resolveExecutingCliPath(src/daemon/executing-cli.ts) returnsprocess.execPathfor a compiled hack. A source invocation (bun index.ts) gives null, which is never eligible for an automatic restart or refresh.src/commands/daemon-systemd.ts):unitRunsExpected,loadedProgramIsExpected(the early gate and the final check before restart) anddescribeSystemdIncompatibleOwner, throughresolveExecutingProgramand a newexecutingProgramhook.src/daemon/launchd.ts):readLaunchdProgramState, which drives refresh eligibility, the restart and stopped-start refusals and the running-mismatch refusal.hackremains only the install and retarget target.hackfirst on PATH as itself. A service running another program with a mismatched daemon is refused asother-programwith ahack daemon installnext step, without a restart.hackand the unit runs the PATH one: a mismatched daemon is refused asother-program, and a compatible one with a pending marker is kept asother-program. A source invocation (null) never restarts.other-programbeforehome-binding; either way nothing is stopped.Review round 15 fix (commit 3b59c56)
reconcileRunningSystemdDaemon(src/commands/daemon-systemd.ts), systemd's loaded copy is stale whileNeedDaemonReload=yesis reported for a unit file that runs this CLI (a repair wrote it, but itsdaemon-reloadfailed).completeReloadBeforeRestart, with the post-reload home recheck).refuseUnexpectedLoadedProgramjudges the newly loadedExecStartjust before the restart.ExecStartis now this CLI, so it restarts and clears the marker.Review round 14 fix (commit fe38f27)
src/commands/daemon-systemd.ts.gateRestartOnLoadedProgramapplies after planning, whatever asked for the restart: a pending-restart marker, a stale-content plan or a version mismatch.ExecStartthat is another hack keeps a compatible daemon (blocked: "other-program"), or refuses a mismatched one.blocked: "unproven-owner".refuseUnexpectedLoadedProgramre-reads the loadedExecStartafter any rewrite or reload and just before every automatic restart. A retained drop-in that still selects another program stops the restart there, with the marker kept.other-program, marker kept); the same with an unreadableExecStartgetsunproven-owner. A missing main program with a retainedExecStartdrop-in override is retargeted and reloaded, but not restarted (other-program, marker kept).Codex findings: restart ordering and loaded ExecStart (commit fa4b583)
restartDaemon(src/commands/daemon.ts) runsrefuseRestartBeforeStopbefore stopping a running hackd. On macOS that isrefuseLaunchdBeforeReplacing(src/commands/daemon-launchd.ts): the loaded job must not bind another home and must be this home's. On Linux it isrefuseSystemdStartBeforeReplacing. A refusal exits 1 withE_DAEMON_SERVICEand the matchingblockedreason, and nothing is stopped. The start after the stop now passesalreadyReplaced, so it goes throughstartReplacementDaemonwith the same idle-gated direct fallback. Tests:HACK_HOME.restart --jsonis refused ashome-binding; the process lives, its pid file is kept, onlylaunchctl printran, and the plist is unchanged.systemctlCLI): an installed, running service whose unit file others can write.restart --jsonis refused asunproven-owner; the daemon lives and no stop was issued.reconcileRunningSystemdDaemon(src/commands/daemon-systemd.ts) now also requires the program systemd has loaded to be this CLI's executable by file identity. That is the single loadedExecStart, drop-ins included, read byreadSystemdLoadedProgram(src/daemon/systemd.ts) fromsystemctl --user show. A drop-in running anotherhackkeeps the daemon (blocked: "other-program"); an unreadable or ambiguousExecStartkeeps it too (blocked: "unproven-owner"). Tests: a drop-inExecStartfor another executable with stale main content and a compatible daemon gives no restart, no reload, an unchanged unit and the daemon kept; two loadedExecStartentries giveunproven-ownerand no restart.Review round 12 fix (commit cb3aafa)
readLaunchdManagerState(src/daemon/launchd.ts) now always querieslaunchctl printfor Hack's label, whether or not the plist file exists, since moving or deleting the file does not unload the job.idleonly on the confirmed not-found answer: exit 113 with "Could not find service".unknown, and so is every other result. That covers loaded config differing from the file,StartInterval,StartCalendarIntervalandWatchPaths.KeepAlive/RunAtLoadreading are gone;parseLaunchdPlistRetryand the parser's boolean value kind are removed.HACK_HOME,HACK_LAUNCHD_DISABLE) cannot query it and never started a launchd job, so it still counts as idle. This is documented in the code.manager-statemode): a missing plist file while the job is still loaded isunknown; a loaded, stopped job whose plist on disk has noKeepAliveisunknown; launchd's confirmed not-found answer isidlewith or without the plist file. Unit tests cover the classifier cases.Review round 11 fixes (commit f204a48)
Changes to
startReplacementDaemon(src/commands/daemon-replace.ts):managerStategate (readServiceManagerStateinsrc/commands/daemon.ts) decides.readSystemdManagerState/classifySystemdManagerState(src/daemon/systemd.ts) runsystemctl --user show --all -p LoadState,ActiveState,SubState,Jobthrough the runner. The manager is idle only whenActiveStateisinactive/failed,SubStateis notauto-restartandJobis empty, or when the unit is not loaded.readLaunchdManagerState(src/daemon/launchd.ts). Superseded by round 12: idle only on launchd's confirmed not-found answer.blocked: "service-pending"and a message that the service may still start hackd.startStoppedDaemonreturns{ kind: "service-refused" | "service-attempted" | "direct-attempted", exitCode, manager }. The newonStartRequestedhooks fire just beforesystemctl startand before the launchd reload/kickstart. An early systemd refusal with an empty trace therefore still falls back, subject to the gate.Tests:
pendingandunknownboth giveservice-pending.none.--allprints an emptyJob=when no job is queued. If it doesn't, the gate readsunknownand refuses (safe direction).KeepAlive) is never idle, so the fallback does not run there while the job stays loaded.Review round 10 fixes (commit 9892b28)
Owner decision: make the guarantee structural.
refuseSystemdStartBeforeReplacing(src/commands/daemon-systemd.ts) also runs the effective-configuration checks the start applies after the write, on the configuration that survives it. These arecheckSystemdLoadedUnitPathsand the newcheckSystemdEffectiveService/checkSystemdEffectivePath(src/daemon/systemd.ts);checkBeforeEnablenow uses the same function. A program retarget checks only the effectivePATH, because the loaded copy still names the missing program. Test: a stale managed unit with a trusted drop-in that puts a world-writable directory onPATHis refused before the stop asrewrite-refused, with nosystemctlmutation and the unit unchanged.startReplacementDaemon(src/commands/daemon-replace.ts, used bystartDaemoninsrc/commands/daemon.tson Linux and macOS):startDaemonDetached).restarted,fallback: "direct"and the service'sblockedreason, and warns to runhack daemon install.--jsondata gains the closedfallbackfield.fallback: "direct"in the data. There is no fallback when the service start worked, is still starting, or was already direct, and a failing direct start keeps the error.startReplacementDaemonwith injected starts, not through a CLI run with a real incompatible daemon.Review round 9 fix (commit be501d4)
src/commands/daemon-systemd.ts,planStoppedStart(the plan) andcomputeUnitRewrite(the new unit text, or why it cannot be written) are now used both byrefuseSystemdStartBeforeReplacing, before a direct daemon is stopped, and by the start itself (prepareSystemdUnitForStartandrewriteUnitForPlan), so the two cannot drift.PATHchecks. The missing-program retarget now runs those program andPATHchecks too.blocked: "rewrite-refused".docs/architecture.md.rewrite-refused, with nosystemctlmutation and a byte-identical unit. The start then refuses for the same reason, and a durable replacement passes. A stale managed unit whose rendered servicePATHincludes a directory others can write is refused before the stop.Codex finding: decide before stopping a direct daemon (commit 0cc5af5)
checkRunningLaunchdDaemon(src/commands/daemon-launchd.ts) now also checks the loaded launchd job before the caller stops this home's incompatible direct daemon (launchdJobBlocksReplacement). If the job's plist binds another home it refuses ashome-binding; if the job is not proven this home's it refuses asunproven-owner. Exit 1,E_DAEMON_SERVICE, and the running daemon is left alone.refuseSystemdStartBeforeReplacing(src/commands/daemon-systemd.ts) runs inhandleRunningDaemonAtStart(src/commands/daemon.ts) before the stop. It refuses up front whenstartViaSystemdIfInstalledwould refuse afterwards: an untrusted unit, a unit whose Hack home cannot be read, a missing program in read-only mode, or loaded files or a program Hack cannot vouch for. A unit serving another home is fine: start then starts this home's hackd directly.HACK_HOME, and one whose job is unproven, are refused without any repair or kickstart; this home's job lets the replacement go on.systemctlstart, stop, restart, reload, enable or reset-failed calls; another home's unit and this home's usable unit let the replacement go on.stopProvenDaemoninhandleRunningDaemonAtStart. An end-to-end run with a real incompatible daemon was not reproduced, because faking a daemon of another version that passes the identity proof isn't practical here.Codex finding: continued unit lines (commit d5c7d88)
Environment=FOO=1 \followed byHACK_HOME=...escaped the per-line check. I chose the simpler, safe option over re-implementing systemd's line joining:findDroppedHomeBinding(src/daemon/systemd.ts) now treats any continued line in the main unit file as a home binding. The unit Hack renders has none. The automatic rewrite is refused withblocked: "home-binding", andhack daemon installreplaces the unit. Tests: a managed, stale unit carryingHACK_HOMEon a continued line is not rewritten when stopped, and is kept when running and compatible; the unit file is byte-identical, with no reload or restart. Unit tests cover continuations of unrelated settings and trailing spaces.Review round 6 fix (commit e6d7355)
getLaunchdServiceStatus(src/daemon/launchd.ts) now records howlaunchctl printanswered, asobservation: "reported" | "not-found" | "failed". Only exit 113 with launchd's "Could not find service" message isnot-found(isLaunchdServiceNotFound); every other failure isfailed.loaded: falsekeeps its meaning for status and doctor display.proveLaunchdJobOwnershipaccepts an unloaded job only whenobservationisnot-found. A failed or missing observation is unproven: repair returnsunproven-ownerwith nobootout, rewrite orbootstrap.printfails with an unrecognized error whilebootoutwould succeed, and onlyprintis called, with the plist unchanged; launchd's own exit-113 answer still allows the refresh. Unit tests cover the classifier and the ownership proof.Codex bot findings, second batch (commit 3f5ebed)
detachedDaemonEnvironment(src/daemon/spawn.ts) dropsHACK_DAEMON_RECONCILE_ONLYnext toHACK_DAEMON_SERVICE_READ_ONLYfor every detached hackd: the reconcile's replacement and the client autostart. A--foregroundstart also ignores the flag (src/commands/daemon.ts). The guard still applies to the post-updatedaemon start --jsonitself. Tests: the spawn environment drops both flags. A CLI test runsdaemon start --foregroundwith the flag set, as a leaked replacement would; the daemon runs, and a reconcile-onlystart --jsonthen reportsnonewith the daemon running.repairLaunchdProgramIfInvalid(src/daemon/launchd.ts) now refuses in two cases:home-binding: the plist'sEnvironmentVariablessetsHACK_HOME, or aHOMEother than the one Hack writes (plistKeepsThisHome, read with the newparseLaunchdPlistEnvironment);unproven-owner: the loaded job is running and its pid is not proven this home's hackd (proveLaunchdJobOwnership, theproveOwnedDaemonProcessproof thatstopuses). A running job without a pid also counts as unproven.startViaLaunchdIfLoadedreports these and does not kickstart such a job. Tests through the macOS test driver: a plist with anotherHACK_HOME, a plist with anotherHOME, and a running job whose pid is not this home's hackd. Each is refused with nobootout/bootstrapand an unchanged plist. Unit tests cover the home check and the job proof. The launchctl stub's job state is now configurable, so refresh tests model a loaded job that runs nothing.Review round 4 fixes (commit 720f51e)
proveOwnershipBeforeRestart(src/commands/daemon-systemd.ts) does a fresh state read just before the restart. It requires an available user manager and no state error. Otherwise it refuses withblocked: "unproven-owner"(refuseUnobservedRestart) and never reuses the earlier snapshot. Test: the finalsystemctl showfails while the home queries succeed; no restart.planDaemonReconcile(src/daemon/reconcile.ts), a pending restart no longer restarts a daemon that already runs this version underHACK_DAEMON_SERVICE_READ_ONLY. It stays pending and is reported (blocked: "read-only", with a warning to runhack daemon install). A version mismatch still restarts. Test: two consecutive read-only starts with a pending reload; the first restarts the mismatched daemon, the second does not, and the marker stays.src/daemon/launchd-plist.ts, root keys are compared exactly as written, without trimming. A root key containing an XML entity or character reference (Program) isunknown. Program values are not trimmed either. Tests: an escapedProgramkey pointing at another existing executable is never replaced (macOS test driver,unknown-program, no launchctl calls); aProgramkey with spaces is not treated asProgram; an escapedProgramArgumentskey; a value with a leading space.Review round 3 fixes (commit 76681cb)
parseLaunchdPlistProgrammoved tosrc/daemon/launchd-plist.tsand is now a small structured reader. It drops comments, tracks dict and array nesting, and readsProgram/ProgramArgumentsonly from the root<dict>. CDATA, unterminated comments, malformed or unbalanced tags, unknown value types and repeated root keys areunknown. Tests: a nestedPrograminEnvironmentVariables, a commented-outProgram, and both combined with a foreign top-level program. The driver tests check that these stayother-programwith no launchctl call and an unchanged plist, plus unit cases for each unknown structure.classifyLaunchdProgramPath(src/daemon/launchd.ts) reportsmissingonly forENOENT/ENOTDIR; every other error isunknown, whichreadLaunchdProgramStateturns intounknown-program.pathExistsis unchanged. Tests: injectedEACCES,EIO,ENOENT,ENOTDIRand a code-less error, plus a realEACCESin the darwin driver (a program inside a mode-000 directory staysunknown-program; skipped when running as root).Codex bot findings (commit 8f8ad89)
refuseUnprovenRestart(src/commands/daemon-systemd.ts) runs the stop path'scheckMainProcessServesHome+decideUnitStopbefore any change and again just before the restart. Cgroup membership of this home's pid is no longer enough. Without the proof, start exits 1 withblocked: "unproven-owner"and changes nothing. Tests: this home's hackd started by a job inside another home's service, and a reload that pointedHACK_HOMEat this home while the main process serves another. In both cases there is no restart, no reload and no unit change.NeedDaemonReload=yes,completeReloadBeforeRestartrunsdaemon-reloadand rechecks the unit's home before restarting. A failed reload returns 1 without restarting and keeps the restart-pending marker. Under read-only the marker is kept after the restart. Test: a reload failure on the repair, a rerun while it still fails (no restart, marker kept), then a rerun that reloads before it restarts and clears the marker.Review round 2 fixes (commit f1b08f7)
startViaLaunchdIfLoaded(src/commands/daemon-launchd.ts) handles"failed"before theonlyIfStaleshortcut: it logs an error and returns 1. Tests cover an unload failure and a load failure after a successful unload (driver with a failing launchctl stub), and the in-process start path (no kickstart, exit 1).parseLaunchdPlistProgram(src/daemon/launchd.ts) givesProgramprecedence, and the newunknownstate is refused (unknown-program).readLaunchdProgramStatetreats only an established absence as missing. Tests coverProgramalone pointing at another existing hack (left alone),Programconflicting withProgramArguments(Programwins), an unreadable program (never replaced), and parser cases.refuseForeignLaunchdProgramruns inrestartDaemon(src/commands/daemon.ts) before the stop. The stopped-service start path also refuses instead of kickstarting another or unreadable program. A darwin CLI test runsdaemon restart --jsonagainst another existing executable: exit 1,E_DAEMON_SERVICE,detail.blocked: "other-program", onlylaunchctl printcalled, plist unchanged.readInvokerInSystemdUnit(src/daemon/systemd.ts) returns unknown for a missing, empty or non-pathControlGroupand for an empty or malformed/proc/self/cgroup.runsInsideJob(daemon-launchd.ts) treats a loaded job with no usable pid as inside. Tests cover each case. The fakes now report aControlGroupfor a running unit, as systemd does.Follow-up: earlier Hack plists count as managed (commit fb18902)
Owner decision:
isHackManagedPlist(src/daemon/launchd.ts) is true for the marker or Hack'sLabel. An unmarked legacy plist that runs this CLI's executable is refreshed and gains the marker. An unmarked plist with another label is stillunmanaged, and another existing program is stillother-program. The systemd header rule is unchanged.Review round 1 fixes (commit 91f8e18)
src/daemon/reconcile.ts(planDaemonReconcile). Linux check:readInvokerInSystemdUnitinsrc/daemon/systemd.ts, used byreconcileRunningSystemdDaemoninsrc/commands/daemon-systemd.ts. macOS check:checkRunningLaunchdDaemoninsrc/commands/daemon-launchd.ts, built onhasAncestorProcessinsrc/daemon/process-ancestry.ts.findDroppedHomeBindinginsrc/daemon/systemd.tsandreadContentBlockindaemon-systemd.tsblock the rewrite.restartReconciledUnitreads the unit's home again after the rewrite and before the restart.readLaunchdProgramState,isSameExecutableFileand the rewrittenrepairLaunchdProgramIfInvalidinsrc/daemon/launchd.ts. The mismatch refusal is incheckRunningLaunchdDaemon.observePostUpdateDaemoninsrc/commands/daemon-upgrade.tsrequires an API answer on this home's socket. The child runs withHACK_DAEMON_RECONCILE_ONLY=1, handled instartDaemoninsrc/commands/daemon.ts.reconcileDaemonAfterUpdate.SYSTEMD_MANAGED_HEADERandisHackManagedUnit(src/daemon/systemd.ts),LAUNCHD_MANAGED_MARKERandisHackManagedPlist(src/daemon/launchd.ts). Docs and the diagram are updated.Docs
docs/architecture.md, section "After an upgrade: a stale service is repaired bystart": the rules, the managed-header policy, the inside-service and home-binding refusals, launchd, the update hook, the diagram and the JSON table (withblocked).docs/versions/v5/upgrade.md,guides/upgrade-from-v4.md,reference/command-contracts.mdanddocs/cli.mdare updated.docs/reference/cli.mdis regenerated.Validation
All runs used Bun 1.4.4 and
umask 022, with tests hermetic: temp HOME, fakesystemctl/loginctl/launchctl, darwin-preload driver.Latest, head 417f741 (merge of next at 081a526):
machine-output-contract+machine-output-success-shapes+sync-spawn-bound+bounded-spawn-sync+update-observe+upgrade-installer+host-command-observation, plus next'scandidate-launcher,native-candidate-launcher,upgrade-check,upgrade-apply,lifecycle-supervisor-observersandlifecycle-supervisor-controller: 264 pass, 16 skipped, 0 fail.bun run check(exit 0; 59 warnings, one more from next's code; the only warnings in this PR's files are the two complexity warnings that already exist on next),bun run privacy:check,bun run docs:prepareplusbun run docs:check, andbun run buildpass. Generators: no diff.bun run build:config-compiler(throwaway zigccwrapper, deleted afterwards),bun run check:native-plan-cli(config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks) andscripts/check-config-compiler-cli.tson its own pass.At 2f26965 (repair only from explicit install):
machine-output-contract+machine-output-success-shapes+sync-spawn-bound+bounded-spawn-sync+update-observe+upgrade-installer+host-command-observation+doctor-command: 175 pass, 0 fail.bun run check(exit 0; no new warnings),bun run privacy:check,bun run docs:prepareplusbun run docs:check, andbun run buildpass. Generators: no diff.src/daemon/systemd.tsandsrc/commands/daemon-systemd.ts, so the native acceptance scripts were not rerun. They last passed at 2088b96.At 6b3a292 (stop not blocked by the reload and start checks):
machine-output-contract+machine-output-success-shapes+sync-spawn-bound+bounded-spawn-sync+update-observe+upgrade-installer+host-command-observation: 143 pass, 0 fail.bun run check(exit 0; no new warnings),bun run privacy:check,bun run docs:prepareplusbun run docs:check, andbun run buildpass. Generators: no diff.src/daemon/systemd.ts, already in the previous round's set, so the native acceptance scripts were not rerun. They last passed at 2088b96, below.At 2088b96 (no automatic repair, relationship directories):
machine-output-contract+machine-output-success-shapes+sync-spawn-bound+bounded-spawn-sync+update-observe+upgrade-installer+host-command-observation: 143 pass, 0 fail.bun run check(exit 0; no new warnings),bun run privacy:check,bun run docs:prepareplusbun run docs:check, andbun run buildpass. Generators: no diff.bun run build:config-compiler(throwaway zigccwrapper, deleted afterwards),bun run check:native-plan-cli(config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks) andscripts/check-config-compiler-cli.tson its own pass.At 7f489a6 (ps and lsof in the process proofs):
tests/system-tool.test.ts): 525 pass, 35 skipped (macOS-only), 0 fail.machine-output-contract,machine-output-success-shapes,sync-spawn-bound,bounded-spawn-sync,update-observe,upgrade-installer,host-command-observation,host-exec-lifetime,host-exec-tty,usage-by-service-cli,usage-details,shared-host-lifecycle,native-authored-host-processes,measure-lsof-scan): 222 pass, 0 fail.bun run check(exit 0; no new warnings),bun run privacy:check,bun run docs:prepareplusbun run docs:check, andbun run buildpass. Generators: no diff.bun run build:config-compiler(throwaway zigccwrapper, deleted afterwards),bun run check:native-plan-cli(config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks) andscripts/check-config-compiler-cli.tson its own pass.At 3e6461c (launchctl and ps by absolute path):
machine-output-contract,machine-output-success-shapes,sync-spawn-bound,bounded-spawn-sync,update-observe,upgrade-installer,native-domain-dns-launchd,projects-observe,local-network-hint-call-sites): 179 pass, 0 fail.bun run check(exit 0; no new warnings),bun run privacy:check,bun run docs:prepareplusbun run docs:check, andbun run buildpass. Generators: no diff.bun run build:config-compiler(throwaway zigccwrapper, deleted afterwards),bun run check:native-plan-cli(config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks) andscripts/check-config-compiler-cli.tson its own pass.At c0f95fc (replacement spawn and post-update re-exec):
machine-output-contract+machine-output-success-shapes+sync-spawn-bound+bounded-spawn-sync+update-observe+upgrade-installer: 138 pass, 0 fail.bun run check(exit 0; the only warnings in changed files are two complexity warnings that already exist onnext),bun run privacy:check,bun run docs:prepareplusbun run docs:check, andbun run buildpass. Generators: no diff.bun run build:config-compiler(throwaway zigccwrapper, deleted afterwards),bun run check:native-plan-cli(config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks) andscripts/check-config-compiler-cli.tson its own pass.At 91cc203 (review round 32):
machine-output-contract+machine-output-success-shapes+sync-spawn-bound+bounded-spawn-sync+update-observe: 121 pass, 0 fail.bun run check(exit 0; no warnings in changed files),bun run privacy:check,bun run docs:prepareplusbun run docs:check, andbun run buildpass. Generators: no diff.bun run build:config-compiler(throwaway zigccwrapper, deleted afterwards),bun run check:native-plan-cli(config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks) andscripts/check-config-compiler-cli.tson its own pass.tests/launchd-isolation.test.tswas rerun after it (69 pass)..pr-body.mdby mistake: a local copy of this PR description used to edit it, holding only the public PR text. 91cc203 removes it. It is not in the branch's final diff, andbun run privacy:checkpasses.At 13a4809 (review round 31):
machine-output-contract+machine-output-success-shapes+sync-spawn-bound+bounded-spawn-sync+update-observe: 121 pass, 0 fail.bun run check(exit 0; no warnings in changed files),bun run privacy:check,bun run docs:prepareplusbun run docs:check, andbun run buildpass. Generators: no diff.bun run build:config-compiler(throwaway zigccwrapper, deleted afterwards),bun run check:native-plan-cli(config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks) andscripts/check-config-compiler-cli.tson its own pass.At 6792a61 (review round 30):
tests/launchd-print.test.ts): 507 pass, 35 skipped (macOS-only), 0 fail.machine-output-contract+machine-output-success-shapes+sync-spawn-bound+bounded-spawn-sync+update-observe: 121 pass, 0 fail.bun run check(exit 0; no warnings in changed files),bun run privacy:check,bun run docs:prepareplusbun run docs:check, andbun run buildpass. Generators: no diff.bun run build:config-compiler(throwaway zigccwrapper, deleted afterwards),bun run check:native-plan-cli(config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks) andscripts/check-config-compiler-cli.tson its own pass.At 240dd08 (merge of next at 34b5053, on top of fix 8610528):
tests/cli-spawn-env-isolation.test.ts): 501 pass, 35 skipped (macOS-only), 0 fail.machine-output-contract+machine-output-success-shapes+sync-spawn-bound+bounded-spawn-sync+update-observe: 121 pass, 0 fail.bun run check(exit 0; no warnings in changed files),bun run privacy:check,bun run docs:prepareplusbun run docs:check, andbun run buildpass. Generators: no diff after the merge commit.bun run build:config-compiler(throwaway zigccwrapper, deleted afterwards),bun run check:native-plan-cli(config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks) andscripts/check-config-compiler-cli.tson its own pass.At 7b3fe8a (launchd PATH check):
tests/cli-spawn-env-isolation.test.ts): 498 pass, 35 skipped (macOS-only), 0 fail.machine-output-contract+machine-output-success-shapes+sync-spawn-bound+bounded-spawn-sync: 107 pass, 0 fail.bun run check(exit 0; no warnings in changed files),bun run privacy:check,bun run docs:prepareplusbun run docs:check, andbun run buildpass. Generators: no diff.bun run build:config-compiler(throwaway zigccwrapper, deleted afterwards),bun run check:native-plan-cli(config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks) andscripts/check-config-compiler-cli.tson its own pass.At 080a785 (review round 27):
tests/cli-spawn-env-isolation.test.ts): 494 pass, 35 skipped (macOS-only), 0 fail.machine-output-contract+machine-output-success-shapes+sync-spawn-bound+bounded-spawn-sync: 107 pass, 0 fail.bun run check(exit 0; no warnings in changed files),bun run privacy:check,bun run docs:prepareplusbun run docs:check, andbun run buildpass. Generators: no diff.src/commands/daemon-systemd.ts, so the native acceptance scripts were not rerun. They last passed at 26d9fbd, below.At 26d9fbd (kept-program checks):
tests/cli-spawn-env-isolation.test.ts): 492 pass, 35 skipped (macOS-only), 0 fail.machine-output-contract+machine-output-success-shapes+sync-spawn-bound+bounded-spawn-sync: 107 pass, 0 fail.bun run check(exit 0; no warnings in changed files),bun run privacy:check,bun run docs:prepareplusbun run docs:checkpass. Generators (generate-machine-output-reference.ts,generate:agent-plugins,docs:cli-reference): no diff.bun run build,bun run build:config-compiler(throwaway zigccwrapper, deleted afterwards),bun run check:native-plan-cli(config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks) andscripts/check-config-compiler-cli.tson its own pass.At af7957d (merge of next at 345dc0d with the #835 envelope rule):
tests/machine-output-contract.test.ts+tests/machine-output-success-shapes.test.ts+tests/sync-spawn-bound.test.ts+tests/cli-spawn-env-isolation.test.ts: 110 pass, 0 fail. This includes the usage-error probe for each daemon command and the generated inventory table.bun run check(exit 0; no warnings in changed files),bun run privacy:check,bun run docs:prepareplusbun run docs:checkpass.scripts/generate-machine-output-reference.ts,bun run generate:agent-plugins,bun run docs:cli-reference) rerun after the commit: no diff.bun run build,bun run build:config-compiler(throwaway zigccwrapper, deleted afterwards),bun run check:native-plan-cli(config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks) andscripts/check-config-compiler-cli.tson its own pass.At effa1dc (review round 24 on top of the security fix):
tests/cli-spawn-env-isolation.test.ts): 489 pass, 35 skipped (macOS-only), 0 fail.tests/sync-spawn-bound.test.ts+tests/bounded-spawn-sync.test.ts: 6 pass.bun run check(exit 0, no warnings in changed files),bun run privacy:check,bun run docs:prepareplusbun run docs:checkpass.bun run docs:cli-reference: no diff.src/commands/daemon-systemd.ts, so the native acceptance scripts were not rerun. They last passed at 01b83fd, below.At 01b83fd (security fix on top of the bf74aaf merge):
tests/sync-spawn-bound.test.ts+tests/bounded-spawn-sync.test.ts: 6 pass, 0 fail.tests/cli-spawn-env-isolation.test.ts): 488 pass, 35 skipped (macOS-only), 0 fail.bun run check(exit 0; its warnings are all in files this PR does not change;bunx ultracite checkon the changed files is clean),bun run privacy:check,bun run docs:prepareplusbun run docs:checkpass.bun run docs:cli-referenceandbun run generate:agent-plugins: no diff.bun run build,bun run build:config-compiler(zigccwrapper, deleted afterwards),bun run check:native-plan-cli(config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks) andscripts/check-config-compiler-cli.tson its own pass.Latest, after merging
nextat bf74aaf (merge commit 172559e; head c15779c):plugins/hack/skills/hack-diagnose/references/error-codes.mdmerged cleanly, andbun run generate:agent-pluginsreproduces it unchanged, withE_DAEMON_SERVICE. (The earlier merge of e47681e in 1dd298b conflicted only on that file's content revision line and was resolved by regenerating it.)compileHackintests/daemon-systemd-cli.test.tsnow usesspawnSyncBoundedwith a 60 s bound, below the test's 90 s timeout. This PR adds no other synchronous spawn. Its two long-lived async children (a foreground daemon and asleepholder) stay onBun.spawn; their tests kill them and they do not block the event loop.tests/sync-spawn-bound.test.ts+tests/bounded-spawn-sync.test.ts: 6 pass, 0 fail.tests/cli-spawn-env-isolation.test.tspasses (it is in the focused set).bun run check(exit 0; its warnings are all in files this PR does not change, andbunx ultracite checkon every changed TS file is clean),bun run privacy:check, andbun run docs:prepareplusbun run docs:checkpass.bun run docs:cli-reference: no diff.bun run buildandbun run build:config-compilerpass.bun run check:native-plan-clipasses all 8 scripts it runs: config-compiler CLI, env plan, host 139, routing 141, endpoint 153, process 145, acquisition 198, config-import 49 checks.scripts/check-config-compiler-cli.tsrun on its own also passes. This Linux host has no systemcc, so the compiler was linked with a throwawayzig ccwrapper (CCandCARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER), deleted afterwards.Earlier rounds:
bun run --cwd packages/cli typecheck,bun run typecheck,bun run check(pre-existing warnings only),bunx ultracite checkon changed tests and new modules,bun run privacy:check, andbun run docs:prepare && bun run docs:checkall pass.tests/daemon-reconcile.test.ts: inside-service and content-block planning, header and home-binding detection, cgroup containment, process ancestry, launchd inside-job keep and refuse, plist marker, executable identity (symlink, hard link, different file), a stale pid file is not a running daemon, a rejected observer, and a daemon that stopped before the child ran.tests/daemon-systemd.test.ts:tests/launchd-isolation.test.ts(darwin driver): another existing program is never retargeted, an unmarked legacy plist with Hack's label and this CLI's program is refreshed and gains the marker, an unmarked plist with another label is not refreshed, and a marked plist with an older environment is refreshed.tests/daemon-systemd-cli.test.ts: reconcile-onlystart --jsonwith nothing running reportsnoneand writes no pid file. The earlier end-to-end managed-unit rewrite and restart still passes.bun run build+./dist/hacksmoke under a temp HOME (HACK_SYSTEMD_DISABLE=1 HACK_LAUNCHD_DISABLE=1 HACK_DAEMON_AUTOSTART_DISABLE=1):start --json, nothing runningnone, no pid filestart --jsonstartedstart --json, runningnonestop --jsonstoppedThe temp daemon was confirmed gone afterwards.
Not verified
ps, and a darwin compatible-running refresh end to end. The repair decisions run through the darwin driver.hack updateacross two releases.hack daemon restartrun from inside the service still stops the service. Only the automatic reconcile is guarded.startruns take the direct-start path, as before this PR.hack daemon install, as item 3 requires.Tracks HACK-2216.
🤖 Generated with Claude Code