Parallel Environments with Worktrees

The pattern

Ravi resolves the active Identity by scanning for .ravi/config.json starting from the current working directory before falling back to ~/.ravi/config.json. This means worktree-based orchestrators get automatic identity isolation for free — no environment variables, no subprocess flags, no global state mutation.

project-root/
├── worktrees/
│   ├── agent-1/
│   │   └── .ravi/config.json   ← agent-1's identity
│   ├── agent-2/
│   │   └── .ravi/config.json   ← agent-2's identity
│   └── agent-3/
│       └── .ravi/config.json   ← agent-3's identity

Any ravi command invoked from inside agent-1/ automatically uses agent-1’s identity. No ravi identity use. No RAVI_CONFIG_DIR. Ravi reads the nearest config.

This works with: Aizen, vibe-tree, amux, Claude Squad, Aperant, Supacode, and any other tool that runs agents inside git worktrees or isolated directories.


Setup: provision and wire a worktree

#!/usr/bin/env bash
# setup-agent-worktree.sh
# Usage: ./setup-agent-worktree.sh <worktree-path> <agent-name>

WORKTREE="$1"
AGENT_NAME="$2"

# Create the git worktree (adjust to your workflow)
git worktree add "$WORKTREE" -b "agent/$AGENT_NAME"

# Provision a Ravi identity for this worktree
IDENTITY=$(ravi identity create --name "$AGENT_NAME" --json)
IDENTITY_UUID=$(echo "$IDENTITY" | jq -r '.uuid')
IDENTITY_EMAIL=$(echo "$IDENTITY" | jq -r '.inbox')
IDENTITY_PHONE=$(echo "$IDENTITY" | jq -r '.phone')

# Write the config file into the worktree root
mkdir -p "$WORKTREE/.ravi"
echo "{\"identity_uuid\": \"$IDENTITY_UUID\"}" > "$WORKTREE/.ravi/config.json"

echo "Worktree $WORKTREE wired to identity $AGENT_NAME"
echo "  email: $IDENTITY_EMAIL"
echo "  phone: $IDENTITY_PHONE"

Once this runs, any ravi command invoked from inside $WORKTREE picks up the correct identity automatically — including commands run by an agent subprocess spawned from that directory.


Fleet provisioning in Python

import subprocess
import json
import pathlib

def provision_worktree(worktree_path: str, agent_name: str) -> dict:
    """
    Provision a Ravi identity and wire it to a worktree directory.
    Returns identity metadata (uuid, email, phone).
    """
    result = subprocess.run(
        ["ravi", "identity", "create", "--name", agent_name, "--json"],
        capture_output=True, text=True, check=True
    )
    identity = json.loads(result.stdout)

    config_dir = pathlib.Path(worktree_path) / ".ravi"
    config_dir.mkdir(parents=True, exist_ok=True)
    (config_dir / "config.json").write_text(
        json.dumps({"identity_uuid": identity["uuid"]})
    )

    return {
        "uuid": identity["uuid"],
        "email": identity["inbox"],
        "phone": identity["phone"],
        "config_dir": str(config_dir),
        "worktree": worktree_path,
    }


# Provision a fleet of 5 parallel agents
import concurrent.futures

agents = [f"worker-{i}" for i in range(1, 6)]
base = "/tmp/fleet"

with concurrent.futures.ThreadPoolExecutor() as pool:
    fleet = list(pool.map(
        lambda name: provision_worktree(f"{base}/{name}", name),
        agents
    ))

for agent in fleet:
    print(f"{agent['email']}{agent['worktree']}")

Each agent’s subprocess or coding-agent session picks up the right identity from its worktree root — no shared state, no env var juggling.


Fleet provisioning in TypeScript

import { execSync } from "child_process";
import * as fs from "fs";
import * as path from "path";

interface AgentContext {
  uuid: string;
  email: string;
  phone: string;
  worktree: string;
}

function provisionWorktree(worktreePath: string, agentName: string): AgentContext {
  const result = execSync(
    `ravi identity create --name "${agentName}" --json`,
    { encoding: "utf-8" }
  );
  const identity = JSON.parse(result);

  const configDir = path.join(worktreePath, ".ravi");
  fs.mkdirSync(configDir, { recursive: true });
  fs.writeFileSync(
    path.join(configDir, "config.json"),
    JSON.stringify({ identity_uuid: identity.uuid })
  );

  return {
    uuid: identity.uuid,
    email: identity.inbox,
    phone: identity.phone,
    worktree: worktreePath,
  };
}

// Provision in parallel
const agents = ["worker-1", "worker-2", "worker-3"];
const base = "/tmp/fleet";

const fleet = await Promise.all(
  agents.map((name) => provisionWorktree(`${base}/${name}`, name))
);

fleet.forEach(({ email, worktree }) =>
  console.log(`${email}  →  ${worktree}`)
);

How identity resolution works (the detail)

When any ravi command runs, the CLI walks up the directory tree from $CWD:

$CWD/.ravi/config.json          ← checked first
$CWD/../.ravi/config.json       ← walks up
...
~/.ravi/config.json             ← global fallback

The first config.json found wins. This means:

  • Placing .ravi/config.json at the worktree root is enough — the CLI finds it regardless of how deep inside the worktree the agent is currently working.
  • If no config is found, commands that require an identity will error with a clear message.
  • ravi identity use writes to ~/.ravi/config.json and is not safe to call from parallel agents — it mutates global state. The per-worktree config pattern avoids this entirely.

When to use RAVI_CONFIG_DIR instead

RAVI_CONFIG_DIR is useful when:

  • You’re launching an agent as a subprocess with no guaranteed CWD (containers, CI jobs, arbitrary process spawners)
  • You need to isolate identity at the environment level, not the directory level
  • You’re running an orchestrator where CWD is shared across multiple agent invocations
# Per-process isolation via env var (for subprocess launchers)
IDENTITY_UUID=$(ravi identity create --name "task-$TASK_ID" --json | jq -r '.uuid')
RAVI_CONFIG=$(mktemp -d)
echo "{\"identity_uuid\": \"$IDENTITY_UUID\"}" > "$RAVI_CONFIG/config.json"

RAVI_CONFIG_DIR="$RAVI_CONFIG" ./run-agent.sh

For worktree-based tools, the per-directory config pattern is simpler and requires no env var injection.


Cleanup

When an agent finishes, delete its identity and worktree:

# Read the identity uuid from the worktree config
UUID=$(jq -r '.identity_uuid' "$WORKTREE/.ravi/config.json")

# Delete the identity (removes all credentials, inbox, secrets)
ravi identity delete "$UUID" --yes

# Remove the worktree
git worktree remove "$WORKTREE" --force

For bulk cleanup of identities created with a naming prefix:

ravi identity list --json | jq -r '.[] | select(.name | startswith("worker-")) | .uuid' \
  | xargs -I{} ravi identity delete {} --yes

Next steps