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
| 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. |
| Not needed | tini (container only), build-essential (no native modules). |
Node
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/npmPin 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:
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git \
jq \
less \
openssh-client \
python3 \
ripgrep \
tzdata \
unzip \
wgetgh, which isn't in the Ubuntu or Debian archives:
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 ghEvery 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
# 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=managedandUV_LINK_MODE=copy. - Put
~/.local/binon the servicePATH, appended and never prepended. The agent can write it. - Leave the distribution's
python3alone. - Provisioning as root: create each level of
~/.localwith the right owner, andcdout of/rootbefore runninguv.
Bishop
npm install -g @agentdeploymentco/bishopThe 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 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 thecodexharness, points somewhere persistent. Put it in the unit rather than.env.- SSH keys cover clone and push, and API calls like
gh pr createneedGH_TOKEN. Check both withgh auth statusandgh api user --jq .login. --auto-updateneeds 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
$HOMEto 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 gcif you want it on your own terms. Bishop also cleans up on its own while running.
The unit
[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.targetRequired:
Environment=HOME=. systemd won't set it from the passwd entry.ReadWritePathscovering$HOME, orProtectHome=read-onlybreaks thread resumption. Same withProtectSystem=strict.WorkingDirectoryat Bishop's directory. That's where Bishop reads.env.<home>/.local/binlast inPATH.ExecStartrunningnodeagainstdist/cli.js, not thebishopsymlink, whose#!/usr/bin/env noderesolves against thatPATH.- No
EnvironmentFile=pointing at the.envBishop already loads.
sudo systemctl enable --now bishop.service
journalctl -u bishop -fReplace 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:
// 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:
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