Switch is designed to run on a dedicated Linux machine - ideally bare metal so the AI agents have real system access and can do useful work. An old laptop, mini PC, or home server works well.
The machine should have:
- Full filesystem access for the AI to read/write code
- Ability to run compilers, tests, docker, etc.
- Network access for git, package managers, APIs
Running in a VM or container defeats the purpose - you want the AI to operate on a real development environment.
- Python 3.11+
- uv package manager
- ejabberd XMPP server (local or remote)
- tmux
- One of:
- OpenCode CLI
- Claude Code CLI
Switch expects an XMPP server. The easiest path is running ejabberd on the same Linux machine as Switch.
Install + start the service:
Debian/Ubuntu:
sudo apt update
sudo apt install ejabberd
sudo systemctl enable --now ejabberd
sudo ejabberdctl statusFedora:
sudo dnf install ejabberd
sudo systemctl enable --now ejabberd
sudo ejabberdctl statusArch:
sudo pacman -S ejabberd
sudo systemctl enable --now ejabberd
sudo ejabberdctl statusConfig file is typically /etc/ejabberd/ejabberd.yml. The one setting you almost always need to align with Switch is hosts: (your XMPP domain). Pick a domain you'll use in JIDs (it can be private/internal if you're only using Tailscale).
Example:
hosts:
- "dev.local"Then restart:
sudo systemctl restart ejabberd- Clone the repository:
git clone <repo-url> switch
cd switch- Install dependencies:
uv sync- Copy and configure environment:
cp .env.example .envEdit .env with your settings:
XMPP_SERVER=your.xmpp.server
XMPP_DOMAIN=your.xmpp.server
XMPP_DISPATCHER_JID=oc@your.xmpp.server
XMPP_DISPATCHER_PASSWORD=your-dispatcher-password
XMPP_RECIPIENT=your-user@your.xmpp.server
EJABBERD_CTL=/path/to/ejabberdctl
# Optional additional dispatchers (OpenCode)
OC_GLM_ZEN_JID=oc-glm-zen@your.xmpp.server
OC_GLM_ZEN_PASSWORD=
OC_GPT_OR_JID=oc-gpt-or@your.xmpp.server
OC_GPT_OR_PASSWORD=
OC_GEMINI_JID=oc-gemini@your.xmpp.server
OC_GEMINI_PASSWORD=
OC_KIMI_CODING_JID=oc-kimi-coding@your.xmpp.server
OC_KIMI_CODING_PASSWORD=opencode.json is local-only (gitignored). Copy the example and point provider baseURLs at your vLLM/Spark hosts:
cp opencode.example.json opencode.jsonEach orchestrator/dispatcher bot needs a dedicated XMPP account:
sudo ejabberdctl register oc dev.local <password>If ejabberdctl requires root on your system (common with distro packages), set in .env:
EJABBERD_CTL="sudo ejabberdctl"Tip: if you're running Switch as a systemd --user service, you'll likely want a sudoers rule that allows only the specific ejabberdctl subcommands Switch uses without prompting:
sudo tee /etc/sudoers.d/switch-ejabberdctl >/dev/null <<'EOF'
# Allow the switch user to manage XMPP accounts/roster without a password prompt.
rin ALL=(root) NOPASSWD: /usr/sbin/ejabberdctl register *, /usr/sbin/ejabberdctl unregister *, /usr/sbin/ejabberdctl add_rosteritem *
EOF
sudo chmod 0440 /etc/sudoers.d/switch-ejabberdctlAdjust username/path to ejabberdctl to match your system (which ejabberdctl).
If ejabberd runs on a different machine, set EJABBERD_CTL to an SSH command:
EJABBERD_CTL="ssh user@host /path/to/ejabberdctl"Switch creates/deletes XMPP accounts dynamically. Ensure ejabberd allows:
- Account registration via ejabberdctl
- Roster manipulation via ejabberdctl
Both Claude Code and OpenCode look for instruction files in the working directory (CLAUDE_WORKING_DIR, defaults to $HOME).
- OpenCode reads
AGENTS.md - Claude Code reads
CLAUDE.md
To share instructions between both backends, create AGENTS.md and symlink CLAUDE.md to it:
# Create your agent instructions
vim ~/AGENTS.md
# Symlink for Claude Code
ln -s ~/AGENTS.md ~/CLAUDE.mdSkills are reusable runbooks/procedures that live in ~/switch/skills/. Both Claude Code and OpenCode can use skills, but they expect different formats:
- Claude Code: Flat
.mdfiles with YAML frontmatter (e.g.,spawn-session.md) - OpenCode: Folder per skill with
SKILL.mdinside (e.g.,spawn-session/SKILL.md)
The sync-to-opencode.py script converts Claude Code skills to OpenCode format:
python ~/switch/scripts/sync-to-opencode.pyThis reads all .md files from ~/switch/skills/ and creates the corresponding folder structure in ~/.config/opencode/skill/. No symlinks needed - the script copies and reformats everything.
Options:
# Preview what would be synced (dry run)
python ~/switch/scripts/sync-to-opencode.py --dry-run
# Custom source/target directories
python ~/switch/scripts/sync-to-opencode.py --source /path/to/skills --target /path/to/opencode/skillsFor a skill to sync successfully, it must have YAML frontmatter with:
name: Lowercase alphanumeric with hyphens only (e.g.,spawn-session, notSpawn_Session)description: What the skill does (max 1024 chars)
Example skill format:
---
name: spawn-session
description: Spawn a new Switch session with a handoff message
---
# Spawn Session
Instructions for spawning a new session...Run the sync script after adding or modifying skills to keep OpenCode up to date.
uv run python -m src.bridgeCopy the service file:
cp switch.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable switch
systemctl --user start switchsystemctl --user start switch # Start service
systemctl --user stop switch # Stop service
journalctl --user -u switch -f # View logs
scripts/run.sh # Run directly (not via systemd)After refactors, run quick import/wiring checks (no XMPP required):
./scripts/smoke.shOptional bridge wiring (temp DB, single-instance lock, SessionManager construct — still no XMPP):
./scripts/smoke.sh --with-bridgeExits non-zero on the first failure with PASS/FAIL per check.
- Start Switch
- Send a message to
oc@your.xmpp.serverfrom your XMPP client - A new contact should appear with the session name
- The AI should respond to your message
switch/
├── src/ # Main application
│ ├── bridge.py # Entry point
│ └── utils.py # XMPP utilities
├── scripts/ # Utility scripts
│ ├── run.sh # Run directly (not via systemd)
│ ├── sessions.sh # List/kill sessions
│ ├── spawn-session.py
│ └── loopback.py
├── docs/ # Documentation
├── pyproject.toml # Dependencies
├── sessions.db # SQLite database (created on first run)
├── output/ # Session output logs
└── .env # Configuration (not committed)