Agent box
A Conn3x circuit that turns a Linux host into a remote box for coding agents: one ordinary user you log in to with your SSH key, with git, tmux, a C toolchain, ripgrep, jq and a current Node.js LTS, plus whichever agent CLIs you list, installed in that user’s own npm prefix. Your laptop stays cool, the agents get the cores and the RAM, and when you close the lid the tmux session is still there in the morning.
It’s the box, not the client. Plain SSH and tmux work with it as they are. So does Orca, which runs agents on a remote host over SSH: Orca creates the git worktrees on the host and brings its own server and Node.js runtime, so what it needs from the box is git, a home directory it can run programs from, an SSH server that allows port forwarding, and the agents themselves. This circuit sets up the first three and installs the agents you name.
The circuit never sees an API key or a subscription. Each agent logs itself in the first time you run it on the box, the same way it would on your laptop.
module "agentbox" { source = "github.com/conn3xorg/circuit-agentbox?ref=v0.1.0"
hosts = { box = "203.0.113.10" } ssh_private_key_file = "~/.ssh/id_ed25519"
user = "dev" ssh_keys = [file("~/.ssh/id_ed25519.pub")] git_name = "Ada Lovelace" git_email = "ada@example.com"
npm_packages = ["@anthropic-ai/claude-code", "@openai/codex", "@google/gemini-cli"]
workspace = "code" repos = ["https://github.com/example/app.git"]}Then run conn3x init and conn3x apply. A second conn3x plan says No changes.
Log in and start work:
ssh dev@203.0.113.10tmux new -A -s agentscd ~/code/app && claudeThe first run of each agent asks you to sign in, or for its API key, and stores that in the user’s home on the box. Nothing in npm_packages is installed unless you list it. The three names above are the npm packages for Claude Code, Codex CLI and Gemini CLI, checked on npm in October 2026; any other npm package with a command works the same way.
For hosts created in the same plan, pass host objects with the host key you generated, so Conn3x checks it without a known_hosts entry. Give each one’s distribution in os too: the circuit picks its packages and NodeSource’s repository by distribution, and Conn3x can’t ask a host that doesn’t exist yet. If os turns out not to match the host, Conn3x stops before configuring it. Set os as well when you put depends_on on this module: depends_on holds back reading the hosts until the apply, so without os the plan fails with Invalid for_each argument.
hosts = { for name, d in digitalocean_droplet.box : name => { address = d.ipv4_address host_key = tls_private_key.host[name].public_key_openssh os = "ubuntu" }}To try it on a throwaway container first, see examples/local.
With circuit-baseline and circuit-hardening
Section titled “With circuit-baseline and circuit-hardening”The account has no password, so it can only log in with one of ssh_keys. The circuit leaves sshd_config alone, though, so other accounts keep whatever sshd allows. To turn password logins off for the whole host, pair it with circuit-baseline, which does that by default.
circuit-hardening sets AllowTcpForwarding no, and Orca can’t start its server on a host that refuses port forwarding. If you use both, give hardening sshd_settings = { AllowTcpForwarding = "yes" }. Plain SSH and tmux don’t need forwarding.
Updating the agents
Section titled “Updating the agents”The circuit installs a package once and then leaves it. Claude Code updates itself. For the others, log in and run npm update -g, or npm install -g @openai/codex@latest for one of them.
What it does
Section titled “What it does”On each host, in order:
- On Rocky Linux and AlmaLinux, installs
epel-releasefrom the distribution’s extras repository, which turns on EPEL, since ripgrep is only there. EPEL stays on afterwards. RHEL doesn’t shipepel-release, so on RHEL turn EPEL on yourself first. - Installs
git,tmux,ripgrepandjq, withbuild-essentialon Debian and Ubuntu, orgcc,gcc-c++andmakeon the Red Hat family, so npm packages with native parts can build. - Adds NodeSource’s repository for
node_majorand installsnodejsfrom it, which includes npm. On Debian and Ubuntu that’shttps://deb.nodesource.com/node_26.xwith the suitenodistro, signed with the key infiles/nodesource-deb.asc. On the Red Hat family it’shttps://rpm.nodesource.com/pub_26.x/nodistro/nodejs/$basearchin/etc/yum.repos.d/nodesource-nodejs.repo, signed withfiles/nodesource-rpm.asc, at priority 9 as NodeSource’s own setup script sets it. Whennode --versionalready reportsnode_major, it does nothing; when it reports an older NodeSource release, it moves it up. - Creates
userwith/home/USERand bash as its login shell, and no password, then adds each ofssh_keysto its~/.ssh/authorized_keys. Keys you add to that file by hand stay. - As that user, sets git’s
user.nameanduser.emailwithgit config --global, and npm’sprefixto~/.localwithnpm config set, so other settings in~/.gitconfigand~/.npmrcstay. - As that user, runs
npm install -gfornpm_packagesthat aren’t installed yet. Their commands land in~/.local/bin, which the shell startup files every supported distribution gives a new user put on thePATHof a login shell. tmux starts login shells, and so doessshwithout a command. - Creates
workspace, owned by the user, and clones each ofreposinto it as that user, unless it’s already there.
Before the circuit runs, the conn3x_host data source reads each host’s distribution over SSH.
Why NodeSource
Section titled “Why NodeSource”Ubuntu 24.04 ships Node.js 18 and Debian 13 ships Node.js 20, both past their end of life, and Claude Code needs 22 or later. NodeSource builds every LTS release for all four supported distributions from one place, which gives the same Node.js everywhere. Both keys have no expiry date. Their fingerprints are:
| File | Fingerprint |
|---|---|
files/nodesource-deb.asc |
6F71 F525 2828 41EE DAF8 51B4 2F59 B5F9 9B1B E0B4 |
files/nodesource-rpm.asc |
242B 8138 31AF 0956 2B6C 46F7 6B88 DA4E 3AF2 8A14 |
What it doesn’t do
Section titled “What it doesn’t do”- It never handles API keys, tokens or subscriptions. Each agent signs itself in.
- It doesn’t give the user sudo. If your agents need it, add it with circuit-users, and think about what an agent with root can do.
- It doesn’t change
sshd_config, open ports or start any service that listens on the network. - It doesn’t keep the clones up to date. A repository is cloned once and then belongs to you and your agents; pulling over it would throw away their work. Only
https://URLs without credentials are accepted, so private repositories are for you to clone after logging in, with your own credentials. - It doesn’t uninstall. Removing a package from
npm_packagesor a URL fromreposleaves it on the host, and deleting the module leaves the user, Node.js and everything else. - On the Red Hat family it doesn’t replace a Node.js from the distribution’s own repositories: its
nodejs-libsandnodejs-npmconflict with NodeSource’s package, and the install fails. Remove them first withdnf remove nodejs nodejs-libs nodejs-npm. Debian and Ubuntu don’t have this problem.
Inputs
Section titled “Inputs”| Name | Type | Default | Description |
|---|---|---|---|
hosts |
any | Required | Hosts to configure, by name. Each value is an address, such as "203.0.113.10", or a host object such as { address = "203.0.113.10", host_key = "ssh-ed25519 AAAA..." }. A host object can also set user, port, private_key_file, jump_hosts and os, the distribution’s ID, such as ubuntu or rocky, which a host created in the same plan needs. The two forms can be mixed. |
ssh_user |
string | "root" |
User to log in as. |
ssh_port |
number | 22 |
SSH port. |
ssh_private_key_file |
string | null |
Path to a private key. When null, Conn3x uses the SSH agent. |
ssh_known_hosts_file |
string | null |
known_hosts file to check host keys against. When null, ~/.ssh/known_hosts. Not used for a host object with host_key. |
ssh_become |
bool | false |
Run commands as root through sudo. Set it when ssh_user isn’t root. |
user |
string | "dev" |
Login name of the account you and your agents work as. Its home is /home/USER. Can’t be root. |
ssh_keys |
list(string) | Required | Public keys that can log in as user, such as [file("~/.ssh/id_ed25519.pub")]. At least one. |
git_name |
string | null |
git’s user.name for user. When null, left as it is. |
git_email |
string | null |
git’s user.email for user. When null, left as it is. |
node_major |
number | 26 |
Node.js major release to install from NodeSource. 26 became the LTS release on 28 October 2026. |
npm_packages |
list(string) | [] |
npm packages to install globally as user, by name without a version, such as ["@anthropic-ai/claude-code", "@openai/codex", "@google/gemini-cli"]. |
workspace |
string | null |
Directory for your checkouts, owned by user: relative to the home directory, such as "code", or absolute. When null, repos are cloned into the home directory. |
repos |
list(string) | [] |
Repositories to clone once into workspace, as https:// URLs without credentials. Each lands in a directory named after the repository, so https://github.com/example/app.git goes to app. |
Outputs
Section titled “Outputs”| Name | Description |
|---|---|
user |
The account the circuit sets up. |
workspace |
Directory the repositories are cloned into, such as /home/dev/code. |
repos |
Path of each cloned repository, by name, such as { app = "/home/dev/code/app" }. |
Requirements
Section titled “Requirements”Requires Conn3x v0.6.9 or later and systemd on each host, with a package mirror, NodeSource, the npm registry and your git hosts reachable from it. On Rocky Linux and AlmaLinux, the host also needs to reach EPEL’s mirrors; on RHEL, EPEL must already be on. The home directory must not be mounted noexec, or nothing in ~/.local/bin runs, Orca’s server included.
Tested on
Section titled “Tested on”Ubuntu 24.04, Debian 13, Rocky Linux 10 and Fedora 45. CI applies examples/local to each on amd64 and checks that the second plan has no changes. It then logs in over SSH as dev with the example’s key, in a login shell, and checks git, Node.js 26, tmux, rg, jq and cc run, that cowsay and semver, installed through npm_packages as stand-ins for agents, run from ~/.local/bin, that git has the example’s email, and that the example repository is cloned and owned by dev. It also checks the account has no password.
The same apply and checks passed by hand on arm64 containers of all four, and on Ubuntu 24.04 and Rocky Linux 10 droplets, with SELinux enforcing on Rocky Linux. On the droplets npm_packages also held @anthropic-ai/claude-code, and claude --version ran as dev. As dev, an SSH local port forward opened, as Orca needs. After a reboot, the plan still had no changes and the checks passed again, and Rocky Linux’s audit log had no denials.
Moving node_major from 24 to 26 was tried by hand on Debian 13 and Rocky Linux 10 containers: the apply upgraded Node.js and the next plan had no changes.
Source
Section titled “Source”- Repository: conn3xorg/circuit-agentbox. This page covers v0.1.0; the repository’s tags list every release.
- Examples: examples/local.
- Related circuits: Baseline, Hardening, Users, Ollama, Qdrant, Open WebUI, LiteLLM, Presidio, Langfuse.