Isolated projects on Linux
Register multiple projects in one Linux VM, authenticate Codex separately, and test project isolation.
Run multiple Convoy projects in one Linux VM with a separate Linux identity, workspace and private HOME for each project. Run all commands on this page inside the VM, except where a step explicitly says to use your PC or source checkout. Provision and start the VM yourself; this workflow does not create it.
This is the experimental OS-1958 workflow. Use a build containing convoy project
and the matching backend apiKeys.core.currentScope query. An older published
CLI or backend may not contain these changes. Registration, private shells and
Codex login have been exercised on a test VM; complete agent/runtime and
container acceptance is still pending.
Start a VM if needed
If you already have a Linux VM, continue with the prerequisites. Otherwise, one option is Multipass. After installing it on your PC, create and enter an Ubuntu VM:
multipass launch 24.04 --name convoy-test --cpus 4 --memory 8G --disk 30G
multipass shell convoy-testInstall system-wide Node.js 22/npm inside it before proceeding.
Install the VM prerequisites
Use a normal Linux account with sudo access and system-wide Node.js 22/npm. Node, Convoy and their parent directories must be root-owned and not writable by other users. A Node installation under the control user's home is unsuitable for the privileged launcher.
sudo apt-get update
sudo apt-get install -y python3 bubblewrap sudo git util-linux ca-certificatesFor an unpublished build, run these commands in your Convoy source checkout:
pnpm --filter @useconvoy/cli build
cd apps/cli
npm pack --ignore-scriptsTransfer the generated package to the VM using SSH or your VM tool. Back inside the VM, install that package, substituting its actual path:
sudo npm install --prefix /opt/convoy /path/to/useconvoy-cli-VERSION.tgz
export PATH="/opt/convoy/node_modules/.bin:$PATH"
convoy project --helpAdd the PATH export to your normal VM account's shell configuration for future terminals. Keep the CLI installation separate from a writable source checkout.
Install the project launcher
Run as the normal VM account, using sudo only where shown:
sudo install -d -m 0755 \
-o "$(id -u)" -g "$(id -g)" /srv/convoy-workspaces
sudo python3 -I \
/opt/convoy/node_modules/@useconvoy/cli/dist/templates/project-isolation/install.py \
--control-user "$(id -un)" \
--workspace /srv/convoy-workspaces \
--package-root /opt/convoy/node_modules/@useconvoy/cli \
--node "$(readlink -f "$(command -v node)")"This installs the restricted launcher, its sudo rule, and
/etc/convoy-projects.json. It then runs a sandbox check and lists the runtimes
it detected. A failed check does not stop the installation; it prints the cause
and the fix. Projects do not start until the check passes. After fixing it,
reload the projects; no reinstall is needed.
When updating Convoy, rerun this installer with the same arguments to update
the installed launcher too. Updating the npm package alone does not replace
/usr/local/libexec/convoy-project-launcher.
Ubuntu AppArmor prerequisite
If registration fails with bwrap: setting up uid map: Permission denied,
inspect the kernel log:
sudo journalctl -k --since "10 minutes ago" --no-pager \
| grep -Ei 'apparmor|DENIED|bwrap' | tail -40
sysctl kernel.apparmor_restrict_unprivileged_userns \
kernel.unprivileged_userns_clone user.max_user_namespacesOn our test VM, AppArmor moved bubblewrap into unprivileged_userns and denied
setpcap and uid_map access. There was no bubblewrap profile. For that case,
install this application-specific exception:
sudo tee /etc/apparmor.d/bwrap >/dev/null <<'PROFILE'
abi <abi/4.0>,
include <tunables/global>
profile bwrap /usr/bin/bwrap flags=(unconfined) {
userns,
}
PROFILE
sudo apparmor_parser -r /etc/apparmor.d/bwrapInspect existing bubblewrap profiles before adding one; do not overwrite an administrator's policy. This exception applies to bubblewrap system-wide, including uses outside Convoy. The global user-namespace restriction remains enabled. Convoy's project UID and filesystem boundary still apply. See Ubuntu's explanation of application-specific namespace policies.
Create and register two projects
Project creation requires CLI 0.73.3 or later. Deploy the matching Convoy backend before
using these commands. Run them as the control user inside your configured VM or
project-isolation devcontainer, outside any project shell.
You need an existing Convoy account and organization. In the web app's API Keys
popup, create a manual unscoped key or an organization-wide key for that organization.
Browser-login and MCP credentials cannot issue worker keys.
Copy the CLI / Convex URL, such as https://your-deployment.convex.cloud.
The MCP endpoint uses .convex.site/api/mcp and is a different URL.
convoy project add a --profile-key a --create
convoy project add b --profile-key b --create
convoy project listEach command asks for the URL, masks the API-key input, and prompts for an
organization, project name and alias. Press Enter to use the
profile key as the project name and accept an alias suggested from that name.
Type another value to override either suggestion. Aliases contain 2-5 letters/numbers and
start with a letter. You can also omit --create and pick an existing project
or Create a new project from the selector. No container rebuild is needed.
Use fresh folder/profile names if your existing A/B tests already use a/b.
Convoy uses the broad key only in the control process. It issues a separate key restricted to the selected project and stores that key in its registration. Click a key name in API Keys to see its Project worker details, source name and status. Inactive workers show Inactive in the expiry column. Worker keys count toward your limit of 100 API keys, shared with every other key you hold, so each project can have its own key on several machines or devcontainers. Issuing a key removes expired workers and workers whose sources were deleted or expired. Scope-narrowed workers still count because restoring source access can reactivate them. They expire no later than the setup key and within one year, even when the setup key has no expiration. They stop authenticating if that source is deleted, expires or no longer covers the project. Keep the setup key valid while its workers are in use. Every project using the same setup key loses backend access when that key expires or is revoked. Worker keys can be deleted but not edited. Existing single-project keys still work when registering an existing project.
For automation, provide all inputs. The API key below is an environment variable you set locally; interactive masked input avoids putting it in process arguments.
convoy project add c --profile-key c --create \
--url "$CONVOY_URL" --api-key "$CONVOY_SETUP_KEY" \
--org your-org-slug --project-name "Project C" --project-alias IPC \
--non-interactiveWith a running service, registration applies without restarting A/B. Otherwise,
start it with convoy service start -b, or use convoy connect --profile a for a
foreground connection. Authenticate your chosen runtime in each private HOME
before running tasks, or assign managed provider credentials. The sections below
cover runtime login and checking the boundary. Pause a service-managed project
before opening its shell.
To clone a public repository during registration, add
--clone https://github.com/owner/repo.git. For a private repository, register an
empty directory, authenticate Git inside its project shell, and clone into ..
Host Git credentials are not inherited.
Recover after a failed add
The CLI prints the new remote project ID before local preparation. If cloning,
profile saving or service registration fails, retry using --project ID
instead of --create, omitting --org, --project-name and --project-alias.
Keep the same folder and profile key. Supply the same
clone source if applicable. Remote project history and local files are retained.
Inspect convoy project list and convoy service status first if the service
response was lost; the operation may already have succeeded.
If creation itself loses its response, inspect the project's alias in Convoy before retrying. A second creation request never silently chooses an existing project. If a worker key was issued but registration failed, its printed key ID lets you revoke the unused key in API Keys. Registration reuses an already saved worker key only after the backend validates its source link. If no valid secret was saved, a retry can issue a replacement.
Acceptance on your VM
Create A and B, authenticate a runtime and complete a real agent task in each.
Confirm distinct project UIDs with convoy project list, and repeat the private
file read/write and worktree checks below. Then add C while A/B remain connected.
Check convoy service status and run a task in C. Finally:
convoy project remove c --yes --interruptC's remote project, task history, workspace and private state must remain. Also try a project-restricted creation key, an expired key, an inaccessible organization and a duplicate alias. Each must fail without adding a project. A failed HTTPS clone after successful creation must print the ID for recovery. These checks require a real Linux environment and runtime credentials; unit tests do not certify the runtime isolation matrix.
Check the project boundary
Enter A and create a marker:
convoy project shell a
id
pwd
echo "private to A" > probe.txt
cat probe.txt
exitThen check from B:
convoy project shell b
id
echo "private to B" > probe.txt
cat /srv/convoy-workspaces/a/probe.txt
exitThe UIDs should differ. Each project must be able to write its own files.
B must not be able to read A's marker. No such file or directory is expected
because A's directory is absent from B's filesystem view. Test the reverse
direction too. This is a smoke check, not proof of the complete isolation model.
A project shell holds the execution lease. Exit it before connecting that project or opening another shell for the same project.
Install and authenticate Codex
At your normal VM prompt, install the shared, root-owned executable:
sudo sh -c 'umask 022 && npm install -g --prefix /usr/local @openai/codex'
/usr/local/bin/codex --versionProjects use every tested runtime (Codex, Claude Code, Cursor, OpenCode) whose binary is root-owned and not group- or world-writable. The launcher checks at each start, so a runtime installed later needs only a project reload. A binary under a project's own HOME is ignored.
Log project A in:
convoy project login a --runtime codexThis runs codex login --device-auth inside A's boundary. With a running
service, it pauses A first, then reloads it and waits until it is online. Add
--interrupt if A has active runs. Open the printed link in your PC's
browser and enter the one-time code. Device-code login may need enabling in
ChatGPT security settings or workspace permissions. See OpenAI's headless login instructions.
Claude Code uses --runtime claude-code, which runs claude auth login.
Cursor uses --runtime cursor, which runs cursor-agent login and prints a
browser link. Cursor is not on npm; see Install Cursor.
OpenCode uses --runtime opencode, which runs opencode auth login; see
Install OpenCode.
Install Cursor
Install the build Convoy tested, root-owned, on x86-64 or ARM64. The same
command upgrades an older Cursor; convoy project check a also prints it while
Cursor is missing:
sudo sh -c 'umask 022 && mkdir -p /usr/local/lib/cursor-agent && curl -fsSL https://downloads.cursor.com/lab/2026.10.22-27dcf5f/linux/$(uname -m | sed s/x86_64/x64/\;s/aarch64/arm64/)/agent-cli-package.tar.gz | tar --no-same-owner --strip-components=1 -xzf - -C /usr/local/lib/cursor-agent && chmod -R go-w /usr/local/lib/cursor-agent && ln -sf /usr/local/lib/cursor-agent/cursor-agent /usr/local/bin/cursor-agent'
/usr/local/bin/cursor-agent --version # 2026.10.22-27dcf5fThen reload the projects. Managed Cursor keys need 2026.08.11 or newer.
Install OpenCode
Install the version Convoy tested, root-owned. convoy project check a also
prints this command while OpenCode is missing:
sudo sh -c 'umask 022 && npm install -g --prefix /usr/local opencode-ai@1.19.5'
/usr/local/bin/opencode --version # 1.19.5Then reload the projects. Managed OpenCode keys need 1.18.18 or newer.
Repeat with convoy project login b --runtime codex. The path /home/convoy/.codex looks
identical in each shell but maps to different private storage. Signing both
projects into the same account gives them that account's provider access;
separate files do not narrow the provider credential's permissions.
Connect and run agents
Close any project shells, then open two terminals into the VM.
Terminal A:
convoy connect --profile aTerminal B:
convoy connect --profile bIn each project's web UI, start a thread using its online profile and Codex.
Ask it to create hello.txt with the project's name and read it back. Both
connections should work concurrently, with logs in their respective terminals.
Then ask B's agent to read A's probe.txt; that must fail too.
Ctrl+C stops a direct connection and its workers. A duplicate connection to the same project should be rejected. To keep both projects running after closing your terminal, use the persistent project service.
Use --name vm-a to name a connection. A backend session conflict is reported
without prompting inside the worker. After checking the existing connection,
use convoy connect --profile a --force to explicitly take it over. Ctrl+C
requests graceful shutdown; after 30 seconds the launcher forcibly stops a
worker that has not exited.
Manage registrations with a running service
With CLI 0.68.0, project add connects a new registration through a running service.
project list --json includes live status. To remove one service connection, use
convoy project remove b --yes --interrupt; other projects keep running. To log
a runtime in, run convoy project login b --runtime codex; it pauses B, runs the
login and reloads B. For other work in the private shell, pause B with
convoy service pause b --interrupt, exit the shell, then run
convoy service reload --profile b --interrupt. See Persistent project service.
Stop, remove and retain data
After stopping B and exiting its shell:
convoy project remove b --yesThis unregisters B locally and retains its workspace, worktrees, private state
and UID reservation. Preserve /var/lib/convoy-projects, the parent workspace
and the control account's Convoy configuration when migrating or recreating
the environment. Do not delete the UID registry while keeping project files.
Removing the last profile disables that project's launcher registration.
Re-adding it restores execution with the same UID. If several profiles point
to the same project, removing one leaves the others usable. project list
also shows pending preparations without a saved profile, including their UID
and path for administrator recovery.
Troubleshooting
Start with convoy project check a. It runs the sandbox probes and checks each
runtime, the project lease and the backend key, then prints the fix for each
failure:
Project a
✓ Sandbox boundary
✓ Codex installed root-owned
✓ Codex config folder
✗ Codex logged in. Run project login a --runtime codex.
- Claude Code not installed
✓ Project lease: held by the project service
✓ Backend accepts the project keyProject preparation failed inside the Linux boundary: check AppArmor logs and namespace support first. Keep the retained state and retry the same registration after fixing the environment. Clone failures can require inspecting the retained checkout before retrying.CODEX_HOME ... does not exist: the launcher predates CLI 0.110.0. Reruninstall.pywith the same arguments; the launcher then creates~/.codexand~/.claudein each project's private HOME.- Missing
apiKeys.core.currentScope: install the matching backend feature; the new CLI alone cannot complete registration against an older backend. - No supported runtime is installed system-wide: install Codex or Claude
Code root-owned, for example with the
npm install -g --prefix /usr/localcommand above, then reload the project.readlink -f "$(command -v codex)"shows the file the launcher checks; it and its parent folders must be root-owned without group or world write. ... is under a user home or nvm:install.pyneeds a Node the sandbox can see. Install a system Node, for example withapt install nodejs, and pass--node /usr/bin/node.
Projects share the VM's network. Filesystem isolation does not protect secrets served by unauthenticated local TCP services or cloud metadata endpoints.
For the container variant, see Multiple Convoy projects.
Browser testing
Continue with Browser automation to install Chromium in the existing VM and test projects A and B without recreating their registrations.
Renew worker credentials
Before a setup key or its worker keys expire, create another manual key in API Keys. For each registered project, run the command below as the control user and select the existing remote project. Supply the new setup key at the masked prompt.
convoy project add YOUR_FOLDER --profile-key YOUR_PROFILE
convoy service reload --profile YOUR_PROFILE --interruptThe reload interrupts that project's current work, so choose an appropriate time. Repeat for each project that used the old source, verify connection and real task execution, then revoke the old setup key. Files and remote projects are retained. There is no bulk renewal command or advance expiry warning yet. Service failures may report a generic authentication error; inspect the worker's API Keys popover for the source status. Normal browser login does not rotate manual setup keys.
To test revocation, delete a disposable setup key and confirm its worker is rejected on the next backend request. Narrowing a source to exclude the project also blocks access; restoring coverage restores access while both keys remain unexpired. Completed work is not undone, and losing backend authorization does not forcibly terminate an already running local agent.