Skip to content
Hallway is out (npm i -g hallway): v0, v1 and v2 have shipped, apart from checking it all on a real Mac. See the roadmap.

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.

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:

Terminal window
hallway service install # loaded now and at every login
hallway service status
hallway service restart
hallway service uninstall

On 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.

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.

apps/slides.yaml
name: Slides
port: 4321
expected: running
service:
command: ["npx", "astro", "dev", "--port", "4321"]
cwd: ~/Code/slides
Terminal window
hallway service add slides # generate and load the job: starts at login, restarted if it exits
hallway stop slides # stop and disable it, so it stays stopped across logins; expected → stopped
hallway start slides
hallway restart slides
hallway logs slides --follow
hallway service rm slides # stop managing it; the app stays declared

Every 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.

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:

FrameworkFlags
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.

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), so vite finds npm’s vite.cmd, never the #!/bin/sh script 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, --persist surviving 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.

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 bin directory first on PATH, so node, npm and npx always resolve, to the same Node that runs the service;
  • builds the rest of PATH from stable directories only, dropping per-shell entries and anything that doesn’t exist;
  • records the Node version, so hallway doctor can warn when it has been uninstalled, and hallway service regenerate can 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.

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 run does: 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:

Terminal window
hallway lost # what was lost, with ids, directories and commands
hallway lost restart <id|port> # start one again, once
hallway lost service <id|port> # start it again, as a service
hallway lost dismiss <id|port> # or --all

Services 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.