Troubleshooting & FAQ

This page covers the errors developers encounter most often — parallel identity race conditions, OTP polling timeouts, and headless auth failures. If you’re stuck, start here.


Parallel agents: identity switching clobbers a concurrent process

Symptom: You have two agent processes running simultaneously. Agent 2 calls ravi identity use agent-2 while Agent 1 is mid-operation. Agent 1’s subsequent commands operate against agent-2’s inbox.

Why it happens: ravi identity use writes to a shared config file (~/.ravi/config.json). Every process on the machine reads that file. There is no per-process isolation from identity use alone.

Fix: Use per-project config files instead of ravi identity use in concurrent processes.

Create a .ravi/config.json in each project’s working directory:

mkdir -p /path/to/agent-workspace-1/.ravi
cat > /path/to/agent-workspace-1/.ravi/config.json <<'EOF'
{ "identity": "agent-1" }
EOF

When Ravi runs inside that directory, it reads the local config instead of the global one.

For dynamic agent spawning, pass the directory explicitly:

# Each agent worker runs from its own directory
cd /agent-workspaces/worker-$WORKER_ID
ravi inbox email --unread --json   # reads worker-$WORKER_ID's identity

See Multi-Agent Setup for a complete example with parallel workers.


OTP / SMS code not arriving (polling timeout)

Symptom: Your script polls ravi inbox sms --unread --json and gets an empty array. The service says it sent the code. You waited 5 seconds with sleep 5 and the poll came back empty.

Why it happens: SMS delivery takes 2–15 seconds depending on carrier routing. A single sleep 5 often fires before the message arrives. There is no guaranteed delivery window.

Fix: Poll in a retry loop with a timeout rather than sleeping once:

# Wait up to 60 seconds for an SMS OTP
CODE=""
for i in $(seq 1 30); do
  CODE=$(ravi inbox sms --unread --json | jq -r '.[0].preview // empty')
  [ -n "$CODE" ] && break
  sleep 2
done

if [ -z "$CODE" ]; then
  echo "ERROR: OTP not received after 60 seconds" >&2
  exit 1
fi

echo "Got OTP: $CODE"

The same pattern applies to email verification links:

LINK=""
for i in $(seq 1 15); do
  LINK=$(ravi inbox email --unread --json | jq -r '.[0].body | match("https://[^ ]+verify[^ ]+").string // empty')
  [ -n "$LINK" ] && break
  sleep 4
done

Note: Mark messages as read after consuming them (ravi inbox sms --mark-read) so they don’t re-appear on subsequent polls during the same session.


Device code expires before you complete login

Symptom: Running ravi auth login in a new environment. The CLI prints a device code and a URL, but by the time you open the browser and complete the OAuth flow, it says “device code expired.”

Why it happens: The OAuth device code has a short validity window (typically 30–60 seconds). This is a standard OAuth constraint, not specific to Ravi.

Fix:

  1. Have the browser open and ready before running ravi auth login.
  2. Copy the device code immediately when it appears — don’t navigate away first.
  3. If the code expires, re-run ravi auth login. A new code is issued each time.

For headless or CI environments, run ravi auth login interactively on the machine once, then use the persisted token for all future runs without re-authenticating:

# Interactive login once
ravi auth login

# Verify token is persisted
ls ~/.ravi/auth.json

# From this point, `ravi` commands work without re-login
# even in non-interactive shells, cron jobs, or Docker containers
# (as long as ~/.ravi/ is mounted or copied into the environment)

PIN entry fails after repeated attempts

Symptom: Ravi prompts for your encryption PIN. After 3 wrong entries it doesn’t accept anything and the CLI appears to hang or error.

Why it happens: The credential vault locks after repeated incorrect PIN entries to prevent brute-force access to the zero-knowledge store.

Fix:

  1. Wait 60 seconds and retry — the lockout window is short.
  2. If you’ve forgotten your PIN, your recovery key (saved to ~/.ravi/recovery-key.txt at setup) can reset vault access: ravi vault recover --key $(cat ~/.ravi/recovery-key.txt).
  3. Store your recovery key somewhere safe immediately after setup. It is the only way to recover a locked vault.

Token refresh fails in a long-running process

Symptom: An agent process that runs for hours (or days) starts getting authentication errors mid-run. Commands that worked at startup now return 401 Unauthorized or token expired.

Why it happens: Ravi auth tokens have a finite lifetime. Short-lived processes (< 1 hour) typically never see this. Long-running background processes that don’t refresh their token will eventually expire.

Fix: Run ravi auth refresh periodically within long-lived processes:

# Refresh token in a background loop (every 30 minutes)
while true; do
  ravi auth refresh >/dev/null 2>&1
  sleep 1800
done &

# Your main agent logic
run_agent

Or integrate the refresh check before operations that require a valid token:

import subprocess, time

def ensure_token_fresh():
    result = subprocess.run(["ravi", "auth", "refresh"], capture_output=True)
    if result.returncode != 0:
        raise RuntimeError(f"Token refresh failed: {result.stderr.decode()}")

# Call before any authenticated Ravi operation
ensure_token_fresh()
email = subprocess.check_output(["ravi", "get", "email", "--json"]).decode()

ravi identity use has no effect inside a Docker container or CI

Symptom: You run ravi identity use my-agent in a CI pipeline or Docker container, and subsequent commands still return errors or use the wrong identity.

Why it happens: ravi identity use writes to ~/.ravi/config.json inside the container. If the home directory is ephemeral (which it is in most CI runners), the file is not persisted between steps.

Fix: Mount your ~/.ravi directory into the container, or write the config file explicitly as part of container setup:

# Docker: mount host credentials into container
docker run -v ~/.ravi:/root/.ravi my-agent-image ravi inbox email --unread --json

# CI (GitHub Actions): write config from a secret
- name: Configure Ravi identity
  run: |
    mkdir -p ~/.ravi
    echo '{"identity":"ci-agent"}' > ~/.ravi/config.json
    echo '${{ secrets.RAVI_AUTH_JSON }}' > ~/.ravi/auth.json

jq returns null instead of the expected field

Symptom: You pipe ravi inbox sms --unread --json to jq -r '.[0].preview' and get null instead of the OTP.

Why it happens: The inbox is empty (no messages arrived yet), so .[0] is null and .preview on null returns null.

Fix: Check for empty array before accessing fields, or use // empty to collapse nulls to an empty string:

# Safe: collapse null to empty string
CODE=$(ravi inbox sms --unread --json | jq -r '.[0].preview // empty')

# Explicit: check length first
MESSAGES=$(ravi inbox sms --unread --json)
COUNT=$(echo "$MESSAGES" | jq length)
if [ "$COUNT" -eq 0 ]; then
  echo "No messages yet"
else
  echo "$MESSAGES" | jq -r '.[0].preview'
fi

Still stuck?