# Local launch templates for `qwen serve` Reference templates for running `qwen serve` as a long-lived background process on a developer workstation. For ordinary local background use, start with the built-in `qwen daemon start` lifecycle commands in the [main guide](./qwen-serve.md#inspect-and-control-a-local-daemon). Use these templates when you need service-manager supervision, startup after reboot, or a custom logging setup. Review the [deployment and security notes](./qwen-serve.md#deployment-and-security-notes) before exposing the listener beyond loopback. > **Audience**: developers who want the daemon up across reboots, with logs going somewhere durable, and a clean `restart-on-failure` story. If you only need the daemon for the duration of a single shell session, plain `qwen serve` (foreground, Ctrl-C to stop) is fine; if you only need a detached local process, `qwen daemon start` is simpler. ## Generate a bearer token (once) ```bash openssl rand -hex 32 > ~/.qwen-serve-token # example user-managed path chmod 600 ~/.qwen-serve-token export QWEN_SERVER_TOKEN="$(cat ~/.qwen-serve-token)" ``` The path and filename above are examples for these service-manager templates, not a built-in location that Qwen Code discovers automatically. Daemons started through `qwen daemon start` can instead opt into the built-in `--save-token` sidecar and `qwen daemon token` command; see [Inspect and control a local daemon](./qwen-serve.md#inspect-and-control-a-local-daemon). The systemd and launchd templates below still pass the token explicitly because they invoke `qwen serve` directly. See [Authentication](./qwen-serve.md#authentication) for the token-selection rules. > **Scope this `export` to the current shell session only.** Don't add it to `~/.bashrc` / `~/.zshrc` — a profile-level export exposes the bearer token to every process spawned from that shell (IDE subprocesses, browser debuggers, `npm` scripts from unrelated projects). For long-running setups, use the systemd `EnvironmentFile=` / launchd `EnvironmentVariables` mechanisms below — both scope the token to just the daemon process. The daemon reads the bearer token from either `--token ` on the CLI or the `QWEN_SERVER_TOKEN` env var (whitespace stripped from both). The TypeScript SDK's `DaemonClient` constructor falls back to `QWEN_SERVER_TOKEN` when no `token` option is passed (PR 27 fallback — clients with the env var set never need to thread the value through their script). One shell-level `export` covers both server boot and SDK client construction (just keep it scoped to the session, per the note above). ## Workspace lifecycle and process boundaries One daemon can host several isolated workspace runtimes under the same listener. Repeat `--workspace` with absolute directories to create explicit startup runtimes; the first is primary. Primary and other explicit startup/static runtimes cannot be removed without restarting the process. Additional workspaces can also be registered while the daemon is running through `POST /workspaces`. Pass `persist: true` to retain a dynamic secondary in the user-level registration store so it is restored on the next start. Untrusted registrations remain visible for diagnostics, bounded file reads, and declared persisted reads but cannot start ACP. Dynamic and persisted-restored secondaries are removable: a normal removal refuses while the runtime is busy, while a forced removal requests termination of active resources and commits logical removal before the same cwd can be re-added. Cleanup is bounded and best-effort after the persistence commit point; failures are logged rather than restoring the removed runtime. Runtime isolation covers cwd, environment overlay, filesystem/trust boundary, workspace services, bridge, Voice lease state, channel worker, and the ACP/MCP resource boundary. Production attempts to preheat the trusted primary ACP child for compatibility; trusted secondaries start on their first runtime-backed command or Session, and untrusted secondaries do not start ACP. Legacy primary routes retain their existing compatibility behavior. Authentication, HTTP rate limits, listener and Voice admission caps, total-session admission, metrics, shutdown, and the process fault radius remain daemon-global. Run separate daemons when those process-level boundaries must be independent. ## Linux: systemd user unit > **Find your `qwen` binary and trusted tool directories first.** The unit file's `ExecStart=` must hold an **absolute path**, and its explicit `PATH` must include trusted directories for tools that daemon sessions need, such as `gh`, `git`, `npm`, and the `node` interpreter used by a script-based `qwen` launcher. Service managers don't read your shell profile. Run `which qwen gh git npm node` in your normal shell, then substitute the actual executable and directories everywhere the template below shows `/PATH/TO/qwen` and `/PATH/TO/USER/BIN`. `~/.config/systemd/user/qwen-serve.service`: ```ini [Unit] Description=Qwen Code daemon (loopback HTTP + SSE) After=network.target [Service] Type=simple # Replace with your project; %h expands to $HOME under user units. WorkingDirectory=%h/project-a # Run `which qwen` to find the absolute path. systemd does NOT read $PATH. ExecStart=/PATH/TO/qwen serve --hostname 127.0.0.1 --port 4170 --workspace %h/project-a --workspace %h/project-b # Replace the first entry with trusted directories that contain qwen's # interpreter and user-installed tools. systemd does not read shell profiles. Environment=PATH=/PATH/TO/USER/BIN:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin # Read the bearer token from a chmod 600 file rather than inlining it # in the unit. `Environment=` would expose the token in the unit file # (typically 644 = world-readable). EnvironmentFile keeps the token in # the user-owned secret file you already created with `chmod 600`. EnvironmentFile=%h/.qwen-serve-token-env Restart=on-failure RestartSec=5 StandardOutput=journal StandardError=journal [Install] WantedBy=default.target ``` Build the env file once (the token file from the setup step holds the raw value; this wraps it in `KEY=value` form so systemd reads it as an env assignment): ```bash echo "QWEN_SERVER_TOKEN=$(cat ~/.qwen-serve-token)" > ~/.qwen-serve-token-env chmod 600 ~/.qwen-serve-token-env ``` Manage: ```bash systemctl --user daemon-reload systemctl --user enable --now qwen-serve.service loginctl enable-linger "$(whoami)" # keep the user manager running after logout / across reboot journalctl --user -u qwen-serve -f # tail logs systemctl --user restart qwen-serve.service # after token rotation systemctl --user disable --now qwen-serve.service ``` Without `loginctl enable-linger`, the user-level systemd instance shuts down when the user logs out and only restarts on next login — on a headless dev box the daemon would not survive an SSH session ending. `enable-linger` is what makes "across reboots" actually work. **System-wide alternative** (shared dev hosts, less common): drop the unit at `/etc/systemd/system/qwen-serve@.service` with `User=%i`, manage via `sudo systemctl enable --now qwen-serve@.service`. Same `[Service]` body otherwise. The non-sensitive `PATH` can remain in `Environment=`, but never put the bearer token there: use `EnvironmentFile=` pointing at the user's `chmod 600` file. Pick user-level + linger for single-user workstations. ## macOS: launchd user agent > **Find your `qwen` binary and trusted tool directories first.** Same constraint as systemd: `ProgramArguments` must hold an **absolute path**, while `EnvironmentVariables.PATH` must include trusted directories containing tools daemon sessions need. Run `which qwen gh git npm node` in your normal shell. Common locations on macOS include `/opt/homebrew/bin` (Homebrew on Apple Silicon), `/usr/local/bin` (Homebrew on Intel and manual installs), `~/.nvm/versions/node/vX.Y.Z/bin` (nvm), and `~/.volta/bin` (Volta). Substitute the actual absolute paths below; launchd does not expand `~` or shell variables. `~/Library/LaunchAgents/com.qwenlm.qwen-serve.plist`: ```xml Label com.qwenlm.qwen-serve ProgramArguments /PATH/TO/qwen serve --hostname 127.0.0.1 --port 4170 --workspace /Users/YOUR-USERNAME/project-a --workspace /Users/YOUR-USERNAME/project-b WorkingDirectory /Users/YOUR-USERNAME/project-a EnvironmentVariables PATH /PATH/TO/USER/BIN:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin QWEN_SERVER_TOKEN PASTE-YOUR-TOKEN-HERE RunAtLoad KeepAlive SuccessfulExit ThrottleInterval 10 StandardOutPath /Users/YOUR-USERNAME/Library/Logs/qwen-serve/out.log StandardErrorPath /Users/YOUR-USERNAME/Library/Logs/qwen-serve/err.log ``` Manage: ```bash mkdir -p ~/Library/Logs/qwen-serve # first time only chmod 600 ~/Library/LaunchAgents/com.qwenlm.qwen-serve.plist # plist holds the inline token launchctl load ~/Library/LaunchAgents/com.qwenlm.qwen-serve.plist launchctl unload ~/Library/LaunchAgents/com.qwenlm.qwen-serve.plist # to stop tail -f ~/Library/Logs/qwen-serve/out.log ~/Library/Logs/qwen-serve/err.log ``` After editing the plist (e.g., rotating the token) you must `unload` then `load` again — `launchctl` does not auto-reload on plist changes the way `systemd daemon-reload` does. Note: each `load` truncates the log files, so save them off if you're investigating an incident before rotating. After starting or restarting either service, open a new daemon session and verify that the tools it needs resolve without changing `PATH` inside the command, for example `command -v gh`. If a tool is missing, add its trusted, absolute containing directory to the service-level `PATH` and reload the service; do not rely on `~/.zshrc`, `~/.bashrc`, or another interactive-shell profile. ## tmux session (interactive supervision) Assumes `QWEN_SERVER_TOKEN` is already exported in your shell (see the setup section above): ```bash tmux new -d -s qwen-serve "qwen serve --hostname 127.0.0.1 --workspace /absolute/path/project-a --workspace /absolute/path/project-b" tmux attach -t qwen-serve # see live logs; Ctrl-b d to detach tmux kill-session -t qwen-serve ``` `tmux new -d` inherits the parent shell's environment, so `QWEN_SERVER_TOKEN` flows through automatically. Best when you want to occasionally watch the daemon's stdout (auth warnings, MCP discovery progress, slow-client warnings) without committing to a service unit. Survives terminal close but not host reboot. ## nohup one-liner (quick + dirty) Assumes `QWEN_SERVER_TOKEN` is already exported in your shell: ```bash nohup qwen serve --hostname 127.0.0.1 \ --workspace /absolute/path/project-a \ --workspace /absolute/path/project-b \ > qwen-serve.log 2>&1 & echo $! # daemon PID; capture if you want to `kill` cleanly later ``` Explicit absolute `--workspace` values keep the daemon independent of the shell's current directory. A client should select one of the advertised `capabilities.workspaces[]` entries and pass its cwd when creating a session. OK for one-off "let me run this in the background while I poke at the API" workflows. **Not recommended** for anything beyond a single session — no restart-on-crash, log file grows unbounded, no clean way to find the daemon if you forget the PID. Prefer tmux for interactive supervision or systemd / launchd for anything you want to outlast a reboot. ## Verifying the daemon is up ```bash curl http://127.0.0.1:4170/health # → {"status":"ok"} curl -H "Authorization: Bearer $QWEN_SERVER_TOKEN" \ http://127.0.0.1:4170/capabilities | jq .protocolVersions # daemon's feature set ``` When auth is configured (`--token` or `QWEN_SERVER_TOKEN`), every normal API route except `/health` on an ordinary loopback bind requires `Authorization: Bearer `; channel webhook ingress always uses its configured `x-qwen-webhook-secret`, and Web Shell document and asset routes remain pre-auth. `--require-auth=true` requires a token at boot and additionally moves loopback `/health` behind the bearer gate without changing webhook authentication. If you started the daemon without a token on the loopback default (the `qwen serve` zero-config path), neither call requires a header and any local process that can reach the primary listener receives full operator API authority, including code execution as the daemon user. The templates above all configure a token, so the `Authorization` header is needed in practice. If `/capabilities` returns `401`, the unit / plist token doesn't match the env-exported token your `curl` is using. ## Token rotation 1. Generate a new token + write the env file the unit references: ```bash openssl rand -hex 32 > ~/.qwen-serve-token chmod 600 ~/.qwen-serve-token echo "QWEN_SERVER_TOKEN=$(cat ~/.qwen-serve-token)" > ~/.qwen-serve-token-env chmod 600 ~/.qwen-serve-token-env ``` (For the launchd / nohup / tmux templates: edit the plist's `` value or re-`export QWEN_SERVER_TOKEN`. Don't forget `chmod 600` on the plist if you regenerate it.) 2. Restart the daemon: - **systemd**: `systemctl --user restart qwen-serve.service` - **launchd**: `launchctl unload ~/Library/LaunchAgents/com.qwenlm.qwen-serve.plist && launchctl load ~/Library/LaunchAgents/com.qwenlm.qwen-serve.plist` - **tmux / nohup**: `kill ` then re-run with the new token in env 3. Update any client SDKs / scripts. The TypeScript SDK's `DaemonClient` reads `QWEN_SERVER_TOKEN` automatically (PR 27 fallback) — re-`export` the new value in any client shell and reconstruct the client. ## Restart and crash behavior Service-manager restart semantics differ across the templates: - **systemd `Restart=on-failure`** — restart only on non-zero exit / signal. A clean SIGTERM (`systemctl stop`) does **not** trigger a restart loop. - **launchd `KeepAlive` with `SuccessfulExit=false`** (the template above) — matches systemd behavior. A bare `` would have respawned even after a clean exit. `ThrottleInterval=10` rate-limits restart storms on persistent failures, mirroring systemd's `RestartSec=5`. - **tmux / nohup** — no automatic restart. A daemon crash leaves you with a dead PID until you re-run. Within a **single daemon process lifetime**, client disconnects recover via SSE `Last-Event-ID` resume per the [Durability model](./qwen-serve.md#durability-model) section of the user guide — the replay ring is in-memory. A daemon **restart** interrupts every in-flight live session and clears the in-memory replay ring. Persisted session content remains on disk and can be loaded against the new daemon process, subject to the same workspace binding rules. See [Loading and resuming a persisted session](./qwen-serve.md#loading-and-resuming-a-persisted-session) for the available recovery routes. ## Scope and related deployment guidance - **Containers and reverse proxies** — this page only provides local service-manager templates. For listener, browser-origin, and certificate requirements, see [Remote Web Shell and phone access](./qwen-serve.md#remote-web-shell-and-phone-access) and [HTTPS / TLS](./qwen-serve.md#https--tls-for-mobile--cross-device-access). Multi-tenant deployments require an external orchestrator with separate daemons for independent security principals. - **Cross-host federation / multi-daemon coordination** — one daemon can host multiple registered workspace runtimes, but daemons do not coordinate session state with each other. - **Windows native service** (`nssm`, Service Control Manager wrapper) — for now use [WSL2](https://learn.microsoft.com/en-us/windows/wsl/) and follow the systemd section above. See the [deployment and security notes](./qwen-serve.md#deployment-and-security-notes) and [Authentication](./qwen-serve.md#authentication) in the main user guide for the current security model and token options.