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:
- Have the browser open and ready before running
ravi auth login. - Copy the device code immediately when it appears — don’t navigate away first.
- 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:
- Wait 60 seconds and retry — the lockout window is short.
- If you’ve forgotten your PIN, your recovery key (saved to
~/.ravi/recovery-key.txtat setup) can reset vault access:ravi vault recover --key $(cat ~/.ravi/recovery-key.txt). - 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?
- Multi-Agent Setup — per-project identity isolation walkthrough
- Authentication — token storage, headless auth, per-project config
- Discord community — fastest path to a human answer