Services and reboots
Hallway keeps the apps you care about running with the OS’s own service manager: launchd jobs on macOS, the mechanism macOS uses for its own background services, systemd user units on Linux, and, best effort, per-user scheduled tasks on Windows (below). There is no PM2 and no second process manager.
Hallway itself
Section titled “Hallway itself”v0 Hallway runs itself the same way, as a user LaunchAgent on macOS, a systemd user unit on Linux, or a scheduled task on Windows:
hallway service install # loaded now and at every loginhallway service statushallway service restarthallway service uninstallOn macOS the job is ~/Library/LaunchAgents/hallway.daemon.plist; on Linux it is
~/.config/systemd/user/hallway-daemon.service; on Windows it is the scheduled task \hallway.daemon (see
Windows). All start at login, not at boot, which is the right behaviour
for a laptop. On Linux, enabling lingering once (loginctl enable-linger $USER) keeps Hallway’s units running
without a login session and starts them at boot; Hallway never enables it itself, and hallway doctor warns while it
is off.
Installing, starting, stopping and restarting these jobs have run for real against launchd, systemd and Windows’ Task Scheduler in CI. Coming back after a logout or a reboot has not been tested yet, on any OS.
Managed services
Section titled “Managed services”v1 An app with a service: block is a managed service: Hallway generates a job for it,
~/Library/LaunchAgents/hallway.app.<slug>.plist on macOS, ~/.config/systemd/user/hallway-app-<slug>.service on
Linux, or the scheduled task \hallway.app.<slug> on Windows, and keeps it alive.
name: Slidesport: 4321expected: runningservice: command: ["npx", "astro", "dev", "--port", "4321"] cwd: ~/Code/slideshallway service add slides # generate and load the job: starts at login, restarted if it exitshallway stop slides # stop and disable it, so it stays stopped across logins; expected → stoppedhallway start slideshallway restart slideshallway logs slides --followhallway service rm slides # stop managing it; the app stays declaredEvery Hallway job has a hallway. label (hallway.daemon, hallway.app.<slug>, and in
fronted mode hallway.caddy and, for a custom domain, hallway.dnsmasq); on Linux the
units are named the same way with hyphens (hallway-daemon.service, hallway-app-<slug>.service). So they are easy
to find, audit and remove, and can’t be confused with jobs you made yourself.
Starting a server for the first time is usually done with hallway run, which declares the
app and creates its job in one step.
Ports and environment
Section titled “Ports and environment”Each managed service gets PORT set to its port and HOST=127.0.0.1 (plus HOSTNAME, for frameworks that read
that). $PORT and $HOST in the command’s arguments are replaced with the app’s values when the job is generated.
Frameworks that ignore PORT need a flag:
| Framework | Flags |
|---|---|
| Vite | --port $PORT --strictPort |
| Astro | --port $PORT |
| Next.js | -p $PORT |
In a YAML command: list, $PORT needs no quoting; on the hallway run command line, quote it ('$PORT') so your
shell doesn’t expand it first.
--strictPort matters for Vite: without it, Vite quietly picks another port when its port is busy, and the app
“moves” away from the address everything else expects.
Windows
Section titled “Windows”v1 Since 0.5.0, best effort. Windows has no user-level launchd or systemd, so the
daemon and each managed app are a scheduled task in Task Scheduler’s root folder, named by its label
(\hallway.daemon, \hallway.app.<slug>). Each is registered as you, with no administrator rights and no stored
password, and starts when you log on.
Task Scheduler restarts a task only when it fails to start, not when its program exits, so the task runs a small
supervisor, hallway supervise (internal, not in --help), inside a headless console so no window opens. The
supervisor runs the command in its directory, appends its output to the usual log files, starts it again 10 seconds
after it exits, and records its state for hallway service status. The task’s files are in
%LOCALAPPDATA%\hallway\tasks\.
hallway stop disables the task as well as ending it, so it stays stopped at the next logon, as on the other OSes.
- Commands are looked up as cmd.exe does (
PATHEXT), sovitefinds npm’svite.cmd, never the#!/bin/shscript npm puts beside it. - Nothing runs while you’re logged off. That would need your password stored with the task, or a service account and administrator rights; Hallway asks for neither, so lingering has no Windows equivalent.
- Checked on GitHub’s Windows runners (install,
hallway run, stop, start, restart,--persistsurviving a killed process, log rotation, doctor and removal), not yet on a real Windows desktop: a real logon starting the tasks, a logoff stopping them, no console window flashing, and a standard (non-administrator) user registering them are all still unverified.
The PATH trap
Section titled “The PATH trap”launchd jobs and systemd units inherit no shell environment, so a job’s PATH has to be written into it. Copying your current shell’s
PATH is a latent bug: some Node version managers put per-shell directories on it. fnm, for example, adds a
fnm_multishells/<pid>_<timestamp>/bin symlink created for one terminal session. Copy that into a job and the
service’s only route to node, npm and npx is a leftover from a shell that closed long ago. Clean it up and every
#!/usr/bin/env node tool the service runs breaks, while the service itself keeps running.
So Hallway’s job generator:
- resolves the command and the Node binary to absolute, real paths in the version manager’s stable install directory, never a shim;
- puts that Node’s own
bindirectory first onPATH, sonode,npmandnpxalways resolve, to the same Node that runs the service; - builds the rest of
PATHfrom stable directories only, dropping per-shell entries and anything that doesn’t exist; - records the Node version, so
hallway doctorcan warn when it has been uninstalled, andhallway service regeneratecan fix it.
Each service writes to ~/.hallway/logs/<slug>.out.log and .err.log. None of launchd, systemd or Task Scheduler rotates those
files, and because they keep them open, renaming doesn’t work either, so the daemon copies and truncates them in place once they
pass a size limit (10 MB by default), keeping three generations.
Lost at last reboot
Section titled “Lost at last reboot”v1 When Hallway starts, it compares the machine’s boot time with the last one it saw. If the machine has rebooted, every app or listener that was alive before the reboot and is not listening now goes into a notice at the top of the dashboard: the doors that closed while you were away.
⚠ Lost at last reboot (Mon 28 Sep 20:05): 6 apps [Review] [Dismiss]Review lists each one: what it was, where it ran from (~/Code/my-app @ main) and the command that started it,
with three actions:
- Restart once starts it again the way
hallway rundoes: the operating system’s service manager owns it, on the port it had. It won’t come back after another reboot. - Make it a service does the same, and also starts it at login and restarts it if it exits.
- Dismiss takes it off the list (the banner’s own Dismiss clears the whole notice).
Something that wasn’t an app yet becomes one when you start it (apps/<slug>.yaml, on its own port, with the command
and directory Hallway recorded). Anything that comes back, by one of these actions or any other way, is marked
back until you dismiss it. “Alive before the reboot” means seen in Hallway’s last scans before it: a server you
stopped a day earlier isn’t listed. The notice survives Hallway restarting, until you dismiss it.
From a terminal:
hallway lost # what was lost, with ids, directories and commandshallway lost restart <id|port> # start one again, oncehallway lost service <id|port> # start it again, as a servicehallway lost dismiss <id|port> # or --allServices that start at login get their start-up grace period (services.startGraceSeconds, 30 seconds by default)
before anything is called lost, so a slow starter isn’t listed by mistake. A server started by one of these actions
shows as ◐ starting while it comes up. The notice is kept in
state/lost.json and is also in /api/state and GET /api/lost, in llms.txt, and in the
MCP server’s get_status; MCP can read it but can’t restart anything.
This only works because Hallway records what it observes while the processes are alive. Once a process is gone, its working directory can’t be recovered by any means, so a tool that starts looking after the reboot is already too late.