Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
49c0550
feat(daemon): reinstall and restart a stale hackd service automatically
roodboi Nov 14, 2026
91f8e18
fix(daemon): guard automatic service reconcile (review round 1)
roodboi Nov 14, 2026
fb18902
fix(daemon): treat earlier hack launchd plists as managed
roodboi Nov 14, 2026
f1b08f7
fix(daemon): fail closed on launchd rewrites and identity checks (rev…
roodboi Nov 14, 2026
8f8ad89
fix(daemon): prove ownership and finish a pending reload before an au…
roodboi Nov 14, 2026
76681cb
fix(daemon): read launchd programs from the root dict and fail closed…
roodboi Nov 14, 2026
720f51e
fix(daemon): fail closed on the final restart read, read-only pending…
roodboi Nov 14, 2026
3f5ebed
fix(daemon): keep reconcile-only out of replacement daemons and prove…
roodboi Nov 14, 2026
e6d7355
fix(daemon): treat a failed launchctl query as unproven, not as not l…
roodboi Nov 14, 2026
d5c7d88
fix(daemon): refuse the automatic unit rewrite when the main file has…
roodboi Nov 14, 2026
0cc5af5
fix(daemon): decide before stopping a direct daemon whether its repla…
roodboi Nov 14, 2026
be501d4
fix(daemon): share the unit-rewrite computation between the pre-stop …
roodboi Nov 14, 2026
9892b28
fix(daemon): check retained drop-ins before a replacement and fall ba…
roodboi Nov 14, 2026
f204a48
fix(daemon): gate the direct-start fallback on an idle manager and an…
roodboi Nov 14, 2026
cb3aafa
fix(daemon): treat launchd as idle only on its confirmed not-found an…
roodboi Nov 14, 2026
fa4b583
fix(daemon): guard restart before its stop and follow the loaded Exec…
roodboi Nov 14, 2026
fe38f27
fix(daemon): gate every automatic restart on the loaded ExecStart
roodboi Nov 14, 2026
3b59c56
fix(daemon): retry a pending reload before judging the loaded program
roodboi Nov 14, 2026
d28eddb
fix(daemon): judge service ownership by the executing CLI, not the ha…
roodboi Nov 14, 2026
9b4b830
fix(daemon): gate launchd rewrites of a running daemon and check reta…
roodboi Nov 14, 2026
356ac41
fix(daemon): check retarget eligibility before stopping an incompatib…
roodboi Nov 14, 2026
62ae3e4
test(daemon): compile the CLI under the fixture's private env
roodboi Nov 14, 2026
1dd298b
Merge remote-tracking branch 'origin/next' into feat/daemon-upgrade-r…
roodboi Nov 15, 2026
172559e
Merge remote-tracking branch 'origin/next' into feat/daemon-upgrade-r…
roodboi Nov 15, 2026
c15779c
test(daemon): bound the compiled-CLI build with the sync spawn helper
roodboi Nov 15, 2026
01b83fd
fix(daemon): check systemd unit trust before any rewrite, reload or r…
roodboi Nov 15, 2026
effa1dc
fix(daemon): report the stopped start's unit-file refusal as untruste…
roodboi Nov 15, 2026
af7957d
Merge remote-tracking branch 'origin/next' into feat/daemon-upgrade-r…
roodboi Nov 15, 2026
26d9fbd
fix(daemon): check the kept program before every automatic service re…
roodboi Nov 15, 2026
080a785
fix(daemon): check the loaded ExecStart pathname last before an autom…
roodboi Nov 15, 2026
7b3fe8a
fix(daemon): check the launchd PATH before any plist install, refresh…
roodboi Nov 15, 2026
8610528
fix(daemon): check the program and PATH before every automatic servic…
roodboi Nov 15, 2026
240dd08
Merge remote-tracking branch 'origin/next' into feat/daemon-upgrade-r…
roodboi Nov 15, 2026
6792a61
fix(daemon): verify launchd's loaded job and the installed program be…
roodboi Nov 15, 2026
13a4809
fix(daemon): require an explicit PATH and Hack's exact command for au…
roodboi Nov 15, 2026
7ffaf75
fix(daemon): trust and bind the launchd plist before any unload or bo…
roodboi Nov 15, 2026
3a49b04
test(daemon): name the plist reload positive control by what it asserts
roodboi Nov 15, 2026
91cc203
chore: remove a PR description scratch file committed by mistake
roodboi Nov 15, 2026
81c4372
fix(daemon): run this CLI for a replacement and verify the replacemen…
roodboi Nov 15, 2026
c0f95fc
fix(update): run the new hack after an update only from a trusted pat…
roodboi Nov 15, 2026
3e6461c
fix(daemon): run launchctl and ps by absolute path
roodboi Nov 15, 2026
7f489a6
fix(daemon): run ps and lsof from fixed trusted paths in the process …
roodboi Nov 15, 2026
2088b96
fix(daemon): never repair unit directories automatically and refuse h…
roodboi Nov 15, 2026
6b3a292
fix(daemon): let hack daemon stop past the reload and start trust checks
roodboi Nov 15, 2026
2f26965
fix(daemon): repair unit directories only from explicit hack daemon i…
roodboi Nov 15, 2026
417f741
Merge remote-tracking branch 'origin/next' into feat/daemon-upgrade-r…
roodboi Nov 15, 2026
6fa8b38
docs(daemon): record the service reconcile across upgrades as an expl…
roodboi Nov 15, 2026
2da1ec1
docs(daemon): state the reconcile model gap without a contradiction
roodboi Nov 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
482 changes: 475 additions & 7 deletions docs/architecture.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ terminal).
- `hack session` — persistent project workspaces (tmux-first)
- `hack doctor` / `hack doctor --fix` — validate and repair local setup
- `hack upgrade check` — read-only upgrade check of this machine and every registered project: a one-screen summary, the projects with findings (worktrees folded; `--verbose` for every checkout), and a plan with one step per kind of change, then the next command; `hack upgrade apply [--only <fix>] [--project <name>] [--dry-run]` runs the plan's apply steps (`.hack/.gitignore` refresh, stale agent files) for every checkout after keeping a snapshot and a journal; `hack upgrade snapshot` and `hack upgrade rollback --rollback-manifest <manifest>` keep and restore the files the upgrade changes
- `hack daemon` — optional local daemon for faster JSON project status; `hack daemon install` starts it at login (launchd on macOS, a systemd user unit on Linux). On Linux without linger it still stops at your last logout and does not start at boot; `loginctl enable-linger` changes that
- `hack daemon` — optional local daemon for faster JSON project status; `hack daemon install` starts it at login (launchd on macOS, a systemd user unit on Linux). On Linux without linger it still stops at your last logout and does not start at boot; `loginctl enable-linger` changes that. After an upgrade, `hack daemon start` (and `hack update`) restarts hackd on the new version when its login service runs the same executable, and rewrites a stale unit or plist that Hack wrote (managed header); `start`, `stop`, `restart` and `install` accept `--json`

Candidate `hack-next doctor` and `hack-v5 doctor` report a missing hackd as optional
for Compose workflows. They do not recommend starting the stable daemon or
Expand Down
8 changes: 6 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1736,7 +1736,7 @@ hack daemon <subcommand> [options]

| Command | Summary |
| --- | --- |
| `hack daemon start` | Start hackd (local daemon) |
| `hack daemon start` | Start hackd (local daemon); reinstalls and restarts a stale login service |
| `hack daemon stop` | Stop hackd |
| `hack daemon restart` | Restart hackd |
| `hack daemon status` | Show hackd status |
Expand All @@ -1756,7 +1756,7 @@ hack daemon <subcommand> [options]

## `hack daemon start`

Start hackd (local daemon)
Start hackd (local daemon); reinstalls and restarts a stale login service

### Usage

Expand All @@ -1769,6 +1769,7 @@ hack daemon start [options]
| Option | Description |
| --- | --- |
| `--foreground` | Run hackd in the foreground (debug) |
| `--json` | Output JSON (machine-readable) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |
Expand All @@ -1787,6 +1788,7 @@ hack daemon stop [options]

| Option | Description |
| --- | --- |
| `--json` | Output JSON (machine-readable) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |
Expand All @@ -1805,6 +1807,7 @@ hack daemon restart [options]

| Option | Description |
| --- | --- |
| `--json` | Output JSON (machine-readable) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |
Expand Down Expand Up @@ -1901,6 +1904,7 @@ hack daemon install [options]
| `--no-run-at-load` | Do not start hackd automatically on login (macOS launchd) |
| `--gui-only` | Only run in GUI sessions (macOS launchd; default) |
| `--no-gui-only` | Run in all session types, including SSH (macOS launchd) |
| `--json` | Output JSON (machine-readable) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/machine-readable-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ success behavior stays covered by command-specific suites.
| `hack config import --json` | Legacy bare object (no `data` envelope) |
| `hack config plan --json` | Native compiler protocol object (`transport_version`, `ok`, result / diagnostics) |
| `hack config validate --json` | Native compiler protocol object (`transport_version`, `ok`, result / diagnostics) |
| `hack daemon install --json` | Envelope; versioned `data.schema`: `hack.daemon-install.v1` |
| `hack daemon restart --json` | Envelope; versioned `data.schema`: `hack.daemon-restart.v1` |
| `hack daemon start --json` | Envelope; versioned `data.schema`: `hack.daemon-start.v1` |
| `hack daemon status --json` | Legacy bare object (no `data` envelope) |
| `hack daemon stop --json` | Envelope; versioned `data.schema`: `hack.daemon-stop.v1` |
| `hack dispatch logs --json` | Legacy bare object (no `data` envelope) |
| `hack dispatch run --json` | Legacy bare object (no `data` envelope) |
| `hack dispatch status --json` | Legacy bare object (no `data` envelope) |
Expand Down
1 change: 1 addition & 0 deletions docs/reference/v5-model-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ must be attached to the eventual exact integration revision.
| Compose detached startup: short job exits before observation; old completed container replaced between samples | **Coverage gap:** no maintained model of external Docker observation consistency | `waitForComposeStartupState` corroborates identities and classifies only the latest snapshot within the remaining start budget. `compose-startup-state.test.ts` covers sub-second interleavings, replacement, failure and deadline; `project-up-startup-state.test.ts` replays the Compose/Docker boundary. `compose-short-oneshot` checks real repeated replacement and sub-second exit zero | Assumes Docker listings eventually describe current containers; two matching identity listings cannot prevent replacement after the final read. Unit bounds are one job, two identities and a 1 s virtual budget. Real-project acceptance and Docker CI remain separate gates. |
| Compose foreground first/second interrupt and lifecycle signal handoff | **Coverage gap:** no maintained model covers the external Compose shutdown protocol | `composeRuntimeBackend.up` isolates the Compose session, forwards each signal once, and waits for child exit/log EOF; `runUpCommand` retires the setup signal owner before attached launch. `tests/compose-foreground-signal.test.ts` covers delayed graceful completion, second interrupt and listener disposal; `tests/e2e/scenarios/compose-foreground-signal.ts` checks both real containers immediately after a process-group SIGINT and CLI exit. | Finite subprocess tests cover one or two interrupts and one child; Docker shutdown success is observed by the live regression, not inferred from child exit. Docker daemon failure, wrapper SIGKILL/crash and an unresponsive Compose client are not qualified. The first interrupt has no forced deadline; Compose owns its configured service stop grace periods. |
| HACK-2184: foreground lifecycle process groups retire before successful cleanup/state removal | **Coverage gap:** no maintained model covers mux acknowledgement versus external group exit | `abortOwnedSession` awaits `createLifecycleSessionRetirement`; capture owned pane descendants before mux teardown, settle command groups before wrapper/reader groups, then remove owned state. `terminateLifecycleProcessGroups` observes absence after TERM and after its one KILL escalation. `tests/lifecycle-group-settlement.test.ts` uses deterministic time/signal boundaries; `tests/project-lifecycle-controller-ownership.test.ts` blocks retirement and verifies cleanup/state cannot complete early. Removing either wait boundary fails its regression. `tests/lifecycle-fifo-retirement.test.ts` exercises real delayed and TERM-resistant FIFO writers plus a separately grouped cooperative worker; only groups outside the wrapper’s forwarded TERM receive an additional identity-checked TERM; `repairProjectLifecycleSessions` uses the same ordered capture/retirement before removing an orphan entry, and `tests/lifecycle-hygiene-retirement.test.ts` gates state removal, retains uncertainty, continues other repairs and exercises a real TERM-resistant FIFO writer; the Docker foreground scenario requires separate app/worker ready markers, polls both named running containers before Ctrl-C, and verifies the one-second TERM handler finished without KILL. `tests/compose-foreground-readiness.test.ts` rejects replayed single-service markers and stale container listings; settlement, absent-session and repair tests cover zombie-only retirement and mixed live members. [Statechart](../diagrams/compose-lifecycle-retirement.svg), [source](../diagrams/compose-lifecycle-retirement.mmd). | Each group phase has a 5 s initial-observation retry budget, 5 s grace and 2 s confirmation after one KILL; each census can overshoot by 2 s. Two phases allow up to 48 s including bounded signal-identity retries, plus capture/mux/optional birth refresh (see lifecycle.md timing table). Second Ctrl-C skips command grace, not fences, exit confirmation or the wrapper reap window; the foreground guard also counts interrupts during Compose shutdown. Failed censuses retry within their deadline; no successful census remains unknown. ESRCH or a successful census containing only zombie members proves retirement; probe EPERM falls through to the zombie-aware census (including macOS zombie-only groups). Delivery EPERM is benign only after observed retirement; a live member refuses, and failed reads retry within the same phase deadline. Other probe errors retain uncertainty, ownership and dependent helpers. Deterministic EPERM-to-ESRCH, mixed-live and denied TERM/KILL cases plus a real held-zombie fixture on macOS/Linux cover these branches; Linux execution does not qualify macOS. Deterministic cases cover selective TERM for two groups; real FIFO fixtures cover a main command waiting for a separate worker group, and the Docker fixture covers two services and one persistent command. OS scheduling, PID reuse outside the active ownership interval, CLI SIGKILL/crash, escaped descendants and zellij retirement are not qualified by these tests. Native launch abandonment excludes canceled startup inputs and budget from missing-birth refresh, while rechecking admission, hook ownership, directory pins, saved owner and mux token; normal start/stop retains full input freshness. A portable adapter regression cancels both startup-input authority and budget; a real adapter regression holds and fails the launch census, cancels, then requires owned mux and state removal. The native recovery fixture retries probe EPERM until ESRCH instead of mistaking the error for absence. FIFO and held-zombie files run in both hosted OS pools. Owned foreground cleanup refreshes only missing births through current pane membership with owner-token checks on both sides; stored births and replacement refusals remain strict. tests/lifecycle-cleanup-followup.test.ts and the controller launch-read failure regression cover refresh, second-interrupt propagation and shared-helper fences. Zellij tracking is dropped because HACK-2215 replaces the lifecycle wrapper and zellij is scheduled for removal in v5.0.0. `up.after` after successful attached Compose return and explicit down hooks are inherited v4.3.0 ordering, documented in lifecycle.md. |
| hackd service reconcile across upgrades (HACK-2216): never stop or restart another home's daemon; never stop a daemon unless its replacement can start or the direct fallback is allowed; nothing reloaded, kickstarted or bootstrapped before trust is established; an update never starts a daemon that was stopped | **Coverage gap:** no maintained model; not model-checked. | Source transitions: the plan decision in `src/daemon/reconcile.ts` (`planDaemonReconcile`, closed `blocked` reasons); systemd prepare and restart in `src/commands/daemon-systemd.ts` and launchd prepare and kickstart in `src/commands/daemon-launchd.ts`; the pre-stop check before a replacement (`refuseSystemdStartBeforeReplacing`, `refuseLaunchdBeforeReplacing`); the final ownership proof and last program and PATH check before a restart (`proveOwnershipBeforeRestart`, `refuseUntrustedProgramLast`); the launchd kickstart gate (`verifyLaunchdJobBeforeKickstart` in `src/daemon/launchd.ts`, loaded job read by `src/daemon/launchd-print.ts`, plist trust and byte binding); the `startReplacementDaemon` fallback in `src/commands/daemon-replace.ts`, gated on the manager being idle and verified against this CLI's version; restart-pending marker recovery; and the post-update hook in `src/commands/daemon-upgrade.ts`, which runs reconcile-only and binds the binary `publishRelease` installed. Regressions: pre-stop refusal before any stop (`tests/daemon-systemd.test.ts`, `tests/daemon-reconcile.test.ts`, `tests/launchd-isolation.test.ts`); final-proof refusal (`tests/daemon-systemd.test.ts`); pending-reload recovery over two runs (`tests/daemon-systemd.test.ts`); replacement version verification and the fallback only on an idle manager (`tests/daemon-reconcile.test.ts`); a plist swapped between check and reload (`tests/launchd-isolation.test.ts`); reconcile-only never starting a stopped daemon (`tests/daemon-reconcile.test.ts`, `tests/daemon-systemd-cli.test.ts`); post-update binary binding (`tests/daemon-reconcile.test.ts`, `tests/upgrade-installer.test.ts`); launchctl print parsing (`tests/launchd-print.test.ts`); fixed `ps` and `lsof` paths (`tests/system-tool.test.ts`). | No finite model and no finite bound. The tests assume one reconcile at a time per Hack home; concurrent `hack daemon` invocations are not modelled. Uncovered: a crash between the stop and the start, power loss, real systemd and launchd behaviour (the tests use a fake `systemctl` and a stub `launchctl`), real `launchctl print` parsing across macOS releases, and concurrent service manager restarts. The Mac launchd pass is pending. |

These rows identify coverage, not closure. A local mutant or spy proves handling,
not that the kernel/provider produces the modeled condition. No model establishes
Expand Down
7 changes: 5 additions & 2 deletions docs/versions/v5/guides/upgrade-from-v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -737,8 +737,11 @@ Source: [current env migration and cleanup guidance](https://github.com/hack-dan
Installing another CLI does not replace an already running daemon. For an
unmanaged daemon, `daemon start` replaces a reachable incompatible daemon;
`daemon restart` is the explicit replacement path. With the newer Linux systemd
service, a unit pointing at a different existing executable needs explicit
`daemon install`: restarting that unit would run its old executable again.
service, `daemon start` restarts a stale service that runs the same executable
(or a link to it), and rewrites its unit when Hack wrote it (managed header). A
hand-written unit is reported, not rewritten. A unit pointing at a different existing
executable needs explicit `daemon install`: restarting that unit would run its
old executable again.
Keep this separate from a side-by-side candidate
trial: the launcher disables daemon autostart, and stable `hackd` stays stopped
while the candidate shares its home. The macOS trials confirmed that the stable
Expand Down
9 changes: 5 additions & 4 deletions docs/versions/v5/reference/command-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,14 @@ Source: `src/commands/env.ts:437 (backendUseSpec)`. No command alias is declared
| `hack log-pipe` | `--format <auto\|docker-compose\|plain>`; `--stream <stdout\|stderr>` | Present |
| `hack doctor` | `--path, -p <dir>`; `--fix`; `--migrate-env-config`; `--domain-migration preview\|apply\|rollback`; `--json`; `--browser-url https://app.hack`; `--browser-result unknown\|works\|fails\|permission-denied`; `--branch <name>`; `--native-cleanup inspect`; `--native-run-mapping inspect\|repair`; `--expect-selection <64-hex>`; `--accept-legacy-device-rebind` | Present |
| `hack crash-capture` | `--path, -p <dir>`; `--since <duration>` | Present |
| `hack daemon start` | `--foreground` | Present |
| `hack daemon stop` | none | Present |
| `hack daemon restart` | none | Present |
| `hack daemon start` | `--foreground`; `--json` | Present |
| `hack daemon stop` | `--json` | Present |
| `hack daemon restart` | `--json` | Present |
| `hack daemon status` | `--json` | Present |
| `hack daemon metrics` | none | Present |
| `hack daemon logs` | `--tail <n>` | Present |
| `hack daemon clear` | none | Present |
| `hack daemon install` | `--run-at-load`; `--no-run-at-load`; `--gui-only`; `--no-gui-only` | Present |
| `hack daemon install` | `--run-at-load`; `--no-run-at-load`; `--gui-only`; `--no-gui-only`; `--json` | Present |
| `hack daemon uninstall` | none | Present |
| `hack secrets set [name]` | `--service <service>` | Present |
| `hack secrets get [name]` | `--service <service>` | Present |
Expand Down Expand Up @@ -332,6 +332,7 @@ Thrown usage errors with an explicit `--json` token become E_USAGE; other thrown
| usage | bare projects[], total nullable, host[], host_total nullable, runtime_ok:boolean, runtime_error nullable; details adds containers[] | failures exit 1; optional error string. No samples exits 0 with warning:no_usage_samples. Watch uses the interactive redraw path even with JSON selected; do not treat watch as a JSON report stream. |
| global status | bare generated_at/caddy/logging/networks/gateway/summary | exits 0 even if summary health false; gateway is retained unsupported data, excluded from public contract |
| daemon status | bare report below | running returns 0; starting/stale/unavailable may return 1 |
| daemon start/stop/restart/install | shared envelope; data schema `hack.daemon-<command>.v1`, manager (systemd/launchd/none), action (none/started/restarted/reinstalled/stopped/installed), stale:boolean, stale_reasons (content/program-missing/version-mismatch), blocked (null/read-only/other-program/unknown-program/unmanaged/home-binding/inside-service/unproven-owner/rewrite-refused/service-pending/untrusted-unit), fallback (null/direct), cli_version, daemon.{running,pid,version,compatible} | failure is `E_DAEMON_SERVICE` with the same data as detail; access refusals `E_PERMISSION`; `start --foreground --json` is `E_USAGE`; exit codes unchanged |
| host ps | bare {generatedAt,snapshotAvailable,commands,lifecycle} | timestamp string, availability boolean; nested telemetry remains specialized |
| session panes/capture/tail | JSON-lines events below | no shared envelope |
| update | command-owned object with ok:boolean, string error on failure | error is not shared error.{code,message}; success varies by check/install outcome; exit follows ok |
Expand Down
12 changes: 9 additions & 3 deletions docs/versions/v5/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,14 @@ backup scripts, source evidence and checks still awaiting real-machine trials.
## The daemon after an upgrade

Installing a CLI does not replace a running daemon. After choosing a durable v5
install, use `hack daemon restart` for an unmanaged incompatible daemon. If a
login service still points at the old executable, use the chosen install's
`hack daemon install`. Keep this outside the shared-home candidate trial;
install, run its `hack daemon start` (or `hack daemon restart`). When the login
service runs that same executable, start restarts hackd on the new version and
rewrites a stale unit that Hack wrote (it carries Hack's managed header) or a
stale launchd plist with Hack's label, including one an earlier Hack wrote.
`hack update` does this by itself when a service is installed and
hackd is running. A hand-written unit, a unit that sets its own Hack home, or a
service that points at another existing executable is reported instead: use the
chosen install's `hack daemon install`. Add `--json` for one machine-readable result. Keep this
outside the shared-home candidate trial;
[daemon and service details](guides/upgrade-from-v4.md#daemon-old-lock-and-missing-checkouts)
explain the difference.
Loading
Loading