---
url: https://bishop.agentdeployment.co/deploying/linux.md
description: >-
  Running Bishop on a Linux host under systemd, with the packages, directories
  and unit it needs.
---

# Linux and systemd

What to install, where things live, and the unit that runs it, for a host that runs the agent as an ordinary user account instead of the [container image](/deploying/container), which already carries all of this.

Verified on Ubuntu 24.04 LTS (x86_64), Node 24.20.0, uv 0.12.10, CPython 3.14.7.

## Requirements

| | |
|---|---|
| Node | 24 or newer. Bishop imports `node:sqlite`. |
| Architecture | x86_64 or arm64. Install on the host that will run it, never copy a tree across architectures. |
| Supervisor | systemd, or any supervisor. Bishop runs in the foreground and never restarts itself. |
| Network | Outbound only, unless you run Teams. See [Network](/deploying/#network). |
| Not needed | `tini` (container only), `build-essential` (no native modules). |

## Node

```sh
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
export NVM_DIR="$HOME/.nvm" && . "$NVM_DIR/nvm.sh"
nvm install v24.20.0

mkdir -p ~/.local/bin
ln -sfn "$HOME/.nvm/versions/node/v24.20.0/bin/node" ~/.local/bin/node
ln -sfn "$HOME/.nvm/versions/node/v24.20.0/bin/npm"  ~/.local/bin/npm
```

Pin the version. The unit needs an absolute path to `node`, since a systemd service gets no login shell and no `nvm`. The symlink in `~/.local/bin` lets the unit survive a Node upgrade without an edit. NodeSource's apt repo works too.

## OS packages

The host is the agent's shell as well as Bishop's, so install what the agent will reach for:

```sh
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
  ca-certificates \
  curl \
  git \
  jq \
  less \
  openssh-client \
  python3 \
  ripgrep \
  tzdata \
  unzip \
  wget
```

`gh`, which isn't in the Ubuntu or Debian archives:

```sh
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
  | sudo tee /usr/share/keyrings/githubcli-archive-keyring.gpg >/dev/null
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
  | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null
sudo apt-get update && sudo apt-get install -y gh
```

Every package here is reachable by a prompt injection. Add to the list deliberately.

nono, for the [sandbox](/running/sandbox), is optional. With the sandbox on, `WorkingDirectory` in the unit below has to be a directory of Bishop's own rather than the agent directory, and `--agent` names the agent.

## Python

```sh
# As the service account, from a directory it can read.
curl -fsSL https://astral.sh/uv/install.sh | sh
uv python install 3.14
```

* Set `UV_PYTHON_PREFERENCE=managed` and `UV_LINK_MODE=copy`.
* Put `~/.local/bin` on the service `PATH`, appended and never prepended. The agent can write it.
* Leave the distribution's `python3` alone.
* Provisioning as root: create each level of `~/.local` with the right owner, and `cd` out of `/root` before running `uv`.

## Bishop

```sh
npm install -g @agentdeploymentco/bishop
```

The package is public, so the host needs no GitHub access.

| | |
|---|---|
| Script the unit runs | `$(npm root -g)/@agentdeploymentco/bishop/dist/cli.js` |
| Upgrade | `npm install -g @agentdeploymentco/bishop@latest`, then restart |
| After a Node upgrade | Reinstall. Under `nvm` the global tree moves with the Node version. |

Both harnesses come out of `node_modules`. There's no separate agent CLI to install. [Install](/install#from-source) covers building from source.

## Credentials

Credentials go in `.env` in Bishop's own directory, mode `0600`, owned by the service account. [Credentials](/reference/credentials) lists every variable.

* `CODEX_HOME`, for the `codex` harness, points somewhere persistent. Put it in the unit rather than `.env`.
* SSH keys cover clone and push, and API calls like `gh pr create` need `GH_TOKEN`. Check both with `gh auth status` and `gh api user --jq .login`.
* `--auto-update` needs git credentials that work non-interactively at runtime.

## Directories

| Directory | Holds |
|---|---|
| Bishop's directory (the unit's `WorkingDirectory`) | `.env`, `bishop.db` and its `-wal`/`-shm`, `.bishop/`, and the agent's workspace unless `--agent` points elsewhere |
| `$HOME` of the service account | Both harnesses' session transcripts. Wiping it breaks every existing thread. |

* Keep them separate. Don't set `$HOME` to Bishop's directory.
* If the agent directory's owner doesn't match the service account, fix the ownership, or run `git config --global --add safe.directory '<agent-dir>'` as that account.
* Set a git identity for the service account (`git config --global user.name ...`), or every commit the agent makes in a fresh snapshot fails with "Author identity unknown".
* Schedule `bishop gc` if you want it on your own terms. Bishop also [cleans up](/deploying/cleanup) on its own while running.

## The unit

```ini
[Unit]
Description=Bishop agent
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=<service-account>
WorkingDirectory=<bishop-dir>
Environment=HOME=<home>
Environment=PATH=/usr/local/bin:/usr/bin:/bin:<home>/.local/bin
ExecStart=<home>/.local/bin/node <npm-root-g>/@agentdeploymentco/bishop/dist/cli.js --agent <repo-or-path> --auto-update
Restart=always
RestartSec=10
SyslogIdentifier=bishop

NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=<home> <bishop-dir>

[Install]
WantedBy=multi-user.target
```

Required:

* `Environment=HOME=`. systemd won't set it from the passwd entry.
* `ReadWritePaths` covering `$HOME`, or `ProtectHome=read-only` breaks thread resumption. Same with `ProtectSystem=strict`.
* `WorkingDirectory` at Bishop's directory. That's where Bishop reads `.env`.
* `<home>/.local/bin` last in `PATH`.
* `ExecStart` running `node` against `dist/cli.js`, not the `bishop` symlink, whose `#!/usr/bin/env node` resolves against that `PATH`.
* No `EnvironmentFile=` pointing at the `.env` Bishop already loads.

```sh
sudo systemctl enable --now bishop.service
journalctl -u bishop -f
```

**Replace a flag in `ExecStart`, don't append one.** A backslash-continued `ExecStart` loses its trailing `\` when a new line gets pasted below the old last one. systemd then ends the directive there, and the line below becomes a bare, invalid line in `[Service]`. `systemctl status` warns that the unit changed on disk but not about the syntax error, so read the file back after editing.

## Letting the agent restart itself

Restarting the unit is a polkit decision, not a sudo one. `/etc/polkit-1/rules.d/49-<service>.rules`:

```javascript
// Let the agent restart its own unit, and nothing else.
polkit.addRule(function (action, subject) {
  if (action.id === "org.freedesktop.systemd1.manage-units" &&
      action.lookup("unit") === "<service>.service" &&
      ["restart", "try-restart", "reload-or-restart"].indexOf(action.lookup("verb")) >= 0 &&
      subject.user === "<service-account>") {
    return polkit.Result.YES;
  }
});
```

This grants `restart` but not `stop`, needs no `sudo`, and leaves `NoNewPrivileges=true` in place. The alternative is letting the agent kill its own process and letting `Restart=always` bring it back. Don't add a sudoers entry, which only works with `NoNewPrivileges` removed.

A restart cuts off the turn that asked for it. See [Restarts](/deploying/#restarts). Claude Code's own permission model may also refuse to let a session restart the service running it, so a person may end up applying the restart by hand either way.

## Verify

As the service account, before starting the service:

```sh
node --version
node -e 'require("node:sqlite"); console.log("sqlite ok")'
git --version && gh --version && rg --version && jq --version
uv python find 3.14
bishop --version
ls -l "$(npm root -g)"/@agentdeploymentco/bishop/node_modules/@openai/   # codex-linux-{x64,arm64}
gh api user --jq .login
bishop gc
```

## Checklist

* \[ ] Node >= 24, pinned, at an absolute path
* \[ ] `ca-certificates curl git jq less openssh-client python3 ripgrep tzdata unzip wget`
* \[ ] `gh` from GitHub's apt repository
* \[ ] nono, and a `WorkingDirectory` separate from the agent, if running with `agent.sandbox`
* \[ ] `uv` and a managed interpreter, `~/.local/bin` appended to `PATH`
* \[ ] `npm install -g @agentdeploymentco/bishop` on the target architecture
* \[ ] `.env` at `0600` in the unit's `WorkingDirectory`
* \[ ] Persistent writable `$HOME`, with `HOME=` set in the unit
* \[ ] `CODEX_HOME`, if running the `codex` harness
* \[ ] A GitHub token `gh api user` accepts, not just an SSH key
* \[ ] Git credentials that work non-interactively, and a global git identity
* \[ ] A polkit rule, if the agent should restart itself
* \[ ] Unprivileged service account: no `sudo`, no `docker` group
