Runtime Hosts
A Runtime Host (storm-runtime) runs agent sessions for one Storm server:
on the server’s own machine, a VM, a Mac or a Windows PC. It connects out to
the server, so the server needs no route to it, and it holds no notes.
The easy way: choose a Runtime Host (or everything) in the installer; it installs the host and enrolls it with your server. The rest of this page is the by-hand route and the details.
It is the same storm-runtime on every platform. Each platform
runs it under the platform’s own service manager, as a dedicated account that
exists for nothing else:
| Linux | macOS | Windows | |
|---|---|---|---|
| Install | apt install storm-runtime (.deb) |
Homebrew or the release tarball, then storm-runtime install as root |
install.ps1, or the release zip, then storm-runtime install as administrator |
| Service | systemd storm-runtime.service |
launchd LaunchDaemon dev.storm.runtime |
Windows Service StormRuntime |
| Account | storm-runtime |
_stormruntime (hidden, no login) |
NT SERVICE\StormRuntime (virtual, no password) |
| Binary | /usr/bin/storm-runtime |
/Library/StormRuntime/bin/storm-runtime |
C:\Program Files\StormRuntime\storm-runtime.exe |
| Config | /etc/storm-runtime/runtime.toml |
/Library/StormRuntime/runtime.toml |
C:\ProgramData\StormRuntime\runtime.toml |
State, key, HOME |
/var/lib/storm-runtime (home/ inside) |
/Library/StormRuntime/state (home/ inside) |
C:\ProgramData\StormRuntime\state (home\ inside) |
| Workspaces | /var/lib/storm-runtime/workspaces |
/Library/StormRuntime/workspaces |
C:\ProgramData\StormRuntime\workspaces |
| Logs | journalctl -u storm-runtime |
/Library/Logs/StormRuntime/storm-runtime.log |
C:\ProgramData\StormRuntime\logs\storm-runtime.log |
On every platform:
- The account is its own. It is never in the server’s group,
adminorstaff, so on a shared machine it cannot read the vaults orstate/auth.db. - The enrollment string is read from a prompt or stdin, never from an
argument, so
psnever shows it. - Stopping the host ends every session and everything the sessions
spawned. The service passes
--exclusive-account, so at start and after shutdown the host also ends any other process left running under its account. Those sessions show asfailed (host_restart)when it comes back. - A revoked host ends its sessions, renames
host.jsontohost.json.revokedand exits 3, and stays stopped. Enroll it again to bring it back; no--forceis needed.
Enrolling is the same everywhere: in the app, open Settings › Hosts &
default agent › Enroll a host, then paste the one-time string at the host’s
enroll prompt.
Linux
sudo apt install storm-runtime# Workspaces are the directories under /var/lib/storm-runtime/workspaces.sudo -u storm-runtime mkdir /var/lib/storm-runtime/workspaces/myproject# In the app: Settings › Hosts & default agent › Enroll a host. Paste the string here:sudo -u storm-runtime storm-runtime enrollsudo systemctl enable --now storm-runtimesudo -u storm-runtime storm-runtime check # proves the whole pathLog the agent CLIs in as storm-runtime, once. The unit’s HOME is
/var/lib/storm-runtime/home, so log in under that account’s home:
sudo -u storm-runtime -H claude # then /loginsudo -u storm-runtime -H opencode auth loginsudo -u storm-runtime -H env CODEX_HOME=/var/lib/storm-runtime/agents/codex \ codex login --device-authCodex keeps its login and its conversations in Storm’s own Codex home,
agents/codex under the host’s state directory, not in ~/.codex: Storm
writes its own session settings there, so log it in with CODEX_HOME set as
above (or codex login --with-api-key). Everything else in that home is
yours: MCP servers you add with codex mcp add (with the same CODEX_HOME),
plugins, ChatGPT apps, hooks. They load in Storm sessions beside your
integrations, and Codex asks its own questions about them, such as whether
to trust a workspace’s own .codex/ settings. Codex sessions run without
Codex’s own sandbox: it needs user namespaces, which this unit forbids, so
the storm-runtime account is the boundary, as it is for every agent. Codex
still asks before the commands it is unsure of and before every Storm tool.
Or keep one Claude login in Storm and give it to the hosts you tick: Claude login sync (optional, for your own machines only).
The CLIs must be on a system path (/usr/local/bin, /usr/bin). The unit’s
ProtectHome hides /home, so a CLI installed into a user’s home is reported
as not installed.
Workspaces elsewhere need the unit widened, and listing them in
/etc/storm-runtime/runtime.toml:
sudo systemctl edit storm-runtime# [Service]# ReadWritePaths=/srv/workThe unit is sandboxed, with one setting deliberately absent:
MemoryDenyWriteExecute, under which OpenCode cannot start. The unit’s
RestartPreventExitStatus=3 keeps a revoked host stopped.
Remove it with sudo apt remove storm-runtime.
macOS
The host runs as a LaunchDaemon under the _stormruntime account, never
as you. It starts at boot whether or not anyone is logged in, and it has the
whole machine: there is no VM in the way, and the job runs at launchd’s
Standard priority rather than Background.
Install. With Homebrew (this repository is the tap; it builds from the release tag, so it needs Rust and takes a couple of minutes):
brew tap dewanshdt/storm https://github.com/dewanshDT/Stormbrew install storm-runtimesudo "$(brew --prefix)/bin/storm-runtime" installOr from the release’s storm-runtime-X-macos-universal.tar.gz (arm64 and
x86_64; ad-hoc signed, not notarized):
tar -xzf storm-runtime-*-macos-universal.tar.gzxattr -d com.apple.quarantine./storm-runtime # only if Gatekeeper refuses itsudo./storm-runtime installinstall is idempotent. It:
- creates the hidden
_stormruntimeaccount and group; - creates the layout in the table above;
- copies the binary to
/Library/StormRuntime/bin, owned by root, so only root can replace what launchd runs; - writes
runtime.tomlif there is none; - writes
/Library/LaunchDaemons/dev.storm.runtime.plistand bootstraps it.
The job does not run yet. launchd keeps it alive only while
state/host.json exists, so an unenrolled host never runs.
Enroll. In the app: Settings › Hosts & default agent › Enroll a host. Then paste the string at this prompt:
cd / && sudo -u _stormruntime /Library/StormRuntime/bin/storm-runtime enrollcd / && sudo -u _stormruntime /Library/StormRuntime/bin/storm-runtime check # proves the whole pathlaunchd starts the host by itself as soon as enrollment writes host.json,
and it comes up online in the app. There is nothing to enable.
Service lifecycle:
sudo launchctl print system/dev.storm.runtime # loaded? running? last exit?sudo launchctl kickstart -k system/dev.storm.runtime # restart itsudo launchctl kill TERM system/dev.storm.runtime # stop it once; launchd restarts ittail -f /Library/Logs/StormRuntime/storm-runtime.logUpgrade by running the new binary’s install, which replaces the daemon’s
copy and restarts the job:
brew upgrade storm-runtime && sudo "$(brew --prefix)/bin/storm-runtime" installUninstall:
sudo /Library/StormRuntime/bin/storm-runtime uninstall # job, plist and binarysudo /Library/StormRuntime/bin/storm-runtime uninstall --purge # also state, logs, config, accountWorkspaces are never deleted. They stay in
/Library/StormRuntime/workspaces until you remove them.
Providers and PATH. launchd gives a job almost no PATH, so the host
sets one explicitly, and it is the same PATH the agents get:
$HOME/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin-
HOMEis/Library/StormRuntime/state/homeandSHELLis/bin/zsh; the shell provider runs/bin/zsh -l. -
Set
path = [...]inruntime.tomlto choose the directories yourself. -
Install the agent CLIs where
_stormruntimecan run them:-
Homebrew (
brew install opencode, or Claude Code’s formula or cask); -
or as the account itself, which puts them in its own
~/.local/bin:Terminal window cd / && sudo -u _stormruntime -H sh -c 'curl -fsSL https://claude.ai/install.sh | bash'
A CLI inside your own home (
~/.local/bin,~/.nvm) is out of the account’s reach, by design, and the host reports it as not installed. -
Log the agent CLIs in for _stormruntime, once. The cd / matters: the
account cannot read your current directory.
Claude Code: use a long-lived token. Its interactive login keeps the credentials in the login Keychain, which a hidden daemon account does not have, and it fails there. The token route works:
-
Run
claude setup-tokenas yourself, and copy the token. -
Store it where only the host’s account can read it, without it ever appearing in a command line:
Terminal window sudo -u _stormruntime sh -c 'umask 077; cat > /Library/StormRuntime/state/claude.env'Type
CLAUDE_CODE_OAUTH_TOKEN=<the token>, then Enter, then Ctrl-D. -
Name that file in
runtime.toml. Listing providers replaces the defaults, so list all three:[[providers]]id = "claude-code"env_file = "/Library/StormRuntime/state/claude.env"[[providers]]id = "opencode"[[providers]]id = "shell"
OpenCode logs in interactively:
cd / && sudo -u _stormruntime -H opencode auth loginStorm never stores or transmits what is in it. Also, a login running as
_stormruntime is ended if the host restarts during it (the
--exclusive-account sweep), so log in while the host is up.
Workspaces, permissions and macOS privacy (TCC).
-
The workspace root is
/Library/StormRuntime/workspaces. It is outside every user home and every privacy-protected folder. -
installgives it two inheritable ACL entries: one for_stormruntime, and one for you (thesudouser, or--operator <user>). A checkout made by you or by an agent stays readable and writable by both, and you can work in it withoutsudo. -
Git checks a repository’s owner and ignores ACLs. It refuses a checkout another account owns (“detected dubious ownership”).
installalready makes the host’s git trust every workspace. Do the same for your own git, once:Terminal window git config --global --add safe.directory '/Library/StormRuntime/workspaces/*' -
To put an existing project in front of an agent, clone or move it into the root:
Terminal window git clone git@github.com:you/project /Library/StormRuntime/workspaces/project -
No Full Disk Access is needed, and none should be granted. A LaunchDaemon cannot answer macOS’s privacy prompts. A root under
/Users(every home,~/Documents,~/Desktop,~/Downloads, iCloud Drive),/Volumes(external and network disks) or/Networkis refused when the host starts. That keeps an agent out of your files by configuration as well as by permission. It also keeps it out of a Storm vault on a mounted share. -
There is no counterpart to systemd’s
ProtectSystemandProtectHome. On macOS the boundary is the account plus file permissions and ACLs. Never add_stormruntimetoadminorstaff, and never give it access to a vault.
Troubleshooting:
| Symptom | Check |
|---|---|
| The host never appears after enrolling | sudo launchctl print system/dev.storm.runtime; ls -l /Library/StormRuntime/state/host.json must exist; read the log. |
claude or opencode shows as not installed |
Run sudo -u _stormruntime -H sh -c 'command -v claude' with the PATH above. A CLI in your home is not reachable; install it with Homebrew or as the account. |
| The host exits at start with “refused” or “root” | A workspace_roots entry is under /Users, /Volumes or /Network, or overlaps a Storm data root. Move it to /Library/StormRuntime/workspaces. |
The host stays stopped and host.json.revoked exists |
It was revoked. Enroll it again (the command above). |
| An agent cannot read a file you put in a workspace | The file came from outside (moved, not copied, from a home folder), so it lacks the inherited ACL. ls -le shows it. Re-copy it, or chmod -R +a "user:_stormruntime allow read,write,append,delete,readattr,writeattr,readextattr,writeextattr,readsecurity,list,search,add_file,add_subdirectory,delete_child,file_inherit,directory_inherit" <dir>. |
Windows
The host runs as the StormRuntime Windows Service under the virtual
account NT SERVICE\StormRuntime, never as you. Windows creates
that account with the service: it has no password, is never an
administrator, and cannot open anyone’s profile. Sessions run on ConPTY,
each inside a Job Object, so ending a session, stopping the service, or the
host dying in any way ends everything the session started. Windows 10 1809 /
Server 2019 or later.
Install. In PowerShell as administrator, either the installer:
irm https://dewanshdt.github.io/Storm/install.ps1 | iexor the release zip by hand (it is not code-signed, so SmartScreen may warn):
Invoke-WebRequest https://github.com/dewanshDT/Storm/releases/latest/download/storm-runtime-windows-x86_64.zip -OutFile $env:TEMP\storm-runtime.zipExpand-Archive -Force $env:TEMP\storm-runtime.zip $env:TEMP\storm-runtime& "$env:TEMP\storm-runtime\storm-runtime.exe" installinstall is idempotent; re-running a newer binary’s install upgrades. It:
- creates the layout in the table above, each directory with a protected
DACL (nothing inherited from
C:\ProgramData, where every user may create files): SYSTEM and Administrators, plus the account where it needs it; - shares the workspace root with you (
--operatornames someone else): you and the account can both modify everything either creates there; - copies the binary into
Program Filesand writesruntime.tomlif there is none; - creates the service as manual, so an unenrolled host never runs, with restart-on-crash and the account’s profile variables in its environment;
- adds
safe.directoryfor the workspaces to the account’s git config, and prints the line for yours.
Enroll, still as administrator (no one can run as a virtual account), and paste the string from Settings › Hosts & default agent › Enroll a host at the prompt. Enrolling starts the service and makes it start with Windows:
& "$env:ProgramFiles\StormRuntime\storm-runtime.exe" enrollOperate: Get-Service StormRuntime, Restart-Service StormRuntime,
Stop-Service StormRuntime; storm-runtime.exe check (as administrator)
proves the enrollment end to end; the log is in C:\ProgramData\StormRuntime\logs.
Remove: & "$env:ProgramFiles\StormRuntime\storm-runtime.exe" uninstall
(--purge also removes the identity, state, logs and config). Workspaces are
always kept.
Agent CLIs must be reachable by the account. The host’s PATH is the
account’s own …\state\home\.local\bin and …\AppData\Roaming\npm, then
the machine PATH as the registry has it when the host starts (restart the
service after installing a CLI machine-wide). claude finds claude.exe or
claude.cmd. A CLI installed in your own profile is not reachable.
- Claude Code needs Git for Windows (its Bash tool runs Git Bash): install it machine-wide.
- Log a CLI in from a Storm shell session on this host — it runs as
the account, in PowerShell — or put a token (
claude setup-token) in anenv_fileunderC:\ProgramData\StormRuntimethatUserscannot read.
Workspaces. Roots under C:\Users (every profile and OneDrive), on a
network share, or on a removable, network or optical drive are refused at
startup; other fixed drives (D:\src) are fine. To work on an existing
checkout, clone or move it into the workspace root.
Troubleshooting:
| Symptom | Check |
|---|---|
| The service stops at once with code 2 | It is not enrolled: run enroll as administrator. |
It stopped with code 3 and host.json.revoked exists |
It was revoked. Enroll it again. |
claude or opencode shows as not installed |
Install it machine-wide or as the account (from a Storm shell session), then Restart-Service StormRuntime. |
| It exits at start naming AM54 | A workspace_roots entry is under C:\Users, a share, or a removable drive. |
| git says “dubious ownership” in your own shell | Run the safe.directory line install printed, as yourself. |
Never share an account with the vaults
Never run a Runtime Host under the server’s account, or under any account
that can read or write the vaults. Storm enforces what a session may do
(read only, its one write vault, never delete) in the server, on the tools
it offers. An agent is a process: it can also do anything its OS account can
do to files. A host running as the server’s user, or as a member of its group,
can edit or delete vault files on disk and bypass all of that. The 2026-10-08
real-run staging had server, vaults and host under one Unix user, and Claude
offered to edit a note’s file directly from a read-only session. Filesystem
permissions are the only boundary there. On a shared machine, check it with
sudo -u storm-runtime ls /srv/storm (on a Mac, sudo -u _stormruntime ls
the vault root), which must be refused.