Skip to content

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, 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 ​

Node24 or newer. Bishop imports node:sqlite.
Architecturex86_64 or arm64. Install on the host that will run it, never copy a tree across architectures.
Supervisorsystemd, or any supervisor. Bishop runs in the foreground and never restarts itself.
NetworkOutbound only, unless you run Teams. See Network.
Not neededtini (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, 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
Upgradenpm install -g @agentdeploymentco/bishop@latest, then restart
After a Node upgradeReinstall. 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 covers building from source.

Credentials ​

Credentials go in .env in Bishop's own directory, mode 0600, owned by the service account. 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 ​

DirectoryHolds
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 accountBoth 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 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. 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 ​