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.

Apps: declared, discovered, observed

An app is a local HTTP server Hallway knows about, identified by a slug such as slides or data-api (lowercase letters, digits and hyphens; it becomes a hostname in fronted mode).

Every app comes from exactly one of three sources, and the dashboard always shows which:

SourceWhere it comes fromDurable?Editable in Hallway?
Declared v0A file, apps/<slug>.yaml, in the data home, written by you, your editor, an agent, the CLI, the API or MCPYesYes
Discovered (no adapter ships yet)An adapter that reads another tool’s registryAs durable as that registryNo, but it can be adopted into a declared app
Observed v0The port scanner: something is listening, and it is neither declared nor discoveredRemembered in state/observed.jsonNo, but it can be claimed or ignored

When an observed listener’s port matches a declared or discovered app, it is not shown twice: it enriches that app with live detail (process ID, working directory, bind address, start time).

The durable kind. The smallest possible declaration is a name and a port:

~/.hallway/apps/slides.yaml
name: Slides
port: 4321

Everything else is optional: a description, what you expect of it (expected), a health-check path, deep links, tags, an icon, proxy options, and a service: block if Hallway should keep it running. The full format is in apps/<slug>.yaml.

A declared app can describe a server Hallway does not manage. If something already runs under its own launchd job, declare it without a service: block and Hallway will front and monitor it without creating a second job:

# apps/notes.yaml: already runs as its own launchd service
name: Notes
description: A notes app that manages its own service
port: 7233
expected: running

Some tools keep their own registry of servers and ports. A discovery adapter would read one, read-only, so those apps appear without being declared twice, switching itself on only when its tool is present on the machine. Adopt would turn a discovered app into a declared one you own.

Not built yet. The daemon has the place for adapters to plug in, and the API and dashboard already know the discovered source, but no adapter ships in the current release. Candidates: the port registry of pm, the dev-server wrapper in the author’s other project Paddock, plain PM2, Docker’s published ports, and process-compose. Until then, every app on the dashboard is declared or observed.

This is the feature that sells the project: “Something has been listening on :4666 for nine days and you have never named it. What is it?”

Every two minutes (and on demand, with hallway scan), the scanner:

  1. Lists listening TCP sockets with lsof on macOS, ss (falling back to /proc) on Linux, or netstat and tasklist on Windows. Without root it sees only your own processes, which is the intended scope. Windows doesn’t expose a process’s working directory, so there Hallway reads it from the process’s own memory with one PowerShell call per scan, which needs no administrator rights for your own processes (elevated, other users’ and 32-bit processes are out of reach). On any OS, a listener whose directory can’t be read shows its command line in the observed tray instead, with a tooltip saying why.
  2. Filters out the noise. On a typical development machine, IDE language servers and agent-tool helpers outnumber real dev servers about ten to one. Hallway skips ports below 1024, the ephemeral range 49152–65535 (where those helpers live), known non-web daemons (postgres, redis-server, sshd and the like), itself, Caddy, anything in ignore.yaml, and ports that belong to declared or discovered apps.
  3. Sniffs for HTTP. It sends GET /. Anything that answers in HTTP, with any status, is a web app; Hallway keeps its status, <title> and favicon link. Non-HTTP listeners go into a collapsed “other listeners” group.
  4. Identifies it. Command lines are often useless (node dist/index.js), so the working directory is the primary clue. From it Hallway walks up to the project root (the nearest package.json, pyproject.toml, Cargo.toml, go.mod or .git), takes a name and description from package.json or the README, reads the git branch and last commit, guesses the framework, notes the bind address, and checks the parent process: a parent of PID 1 (on Windows, a parent that is no longer running) means the session that started it is gone, so it is flagged orphaned.
  5. Labels worktree siblings. Several copies of one app running from different git worktrees show as repo @ branch, so they are no longer indistinguishable.
  6. Remembers. Each observation is saved under a stable ID (a hash of project root, port and normalised command, since PIDs change on every restart), with when it was first and last seen.

That last step matters more than it looks. Once a process dies, its working directory can’t be recovered. Recording it while it is alive is the only way Hallway can later tell you what was lost at the last reboot.

Claim (a button in the dashboard, hallway claim, the API, or MCP) writes apps/<slug>.yaml from the observation:

  • the slug, suggested from the package name;
  • port: the port it is already on. Never a new one (see Your ports stay put);
  • expected: adhoc;
  • project.dir, from the observed working directory;
  • the command that started it, as a commented-out service: block you can switch on later.

Ignore hides a listener for good, by port, by process name or by directory (you choose), by adding it to ignore.yaml.