Control coding agents (Claude Code, OpenCode, DeepSeek Harness) running on your laptop or VPS from your Android phone, over end-to-end encrypted Nostr. No accounts and no CodeDeck server: the phone and the bridge pair by scanning a QR code and talk through ordinary Nostr relays — public ones, or your own.
Two halves exchange NIP-44 encrypted messages through one or more Nostr relays of your choice. The relays only store and forward ciphertext: they cannot read your sessions, and no account or CodeDeck-run service sits in between.
- The bridge —
codedeck-bridge, a headless service that runs agent sessions on the machine where your code lives and exposes them over encrypted Nostr. It ships as a Docker image and as Linux archives (docs/BRIDGE.md). - The Android app — drives those sessions from your phone: review plans,
approve tool permissions, answer the agent's questions, switch models, and
chat with several sessions at once (
docs/CLIENT.md).
Pairing is a one-time QR scan; it survives restarts on both ends.
- Several concurrent sessions, switchable from one screen
- Plan approval, permission cards and agent questions on the phone
- Transcripts that survive restarts and offline gaps (ranged sync)
- Per-session mode, model and effort, plus custom AI provider profiles (Kimi K3, OpenRouter, any Anthropic-compatible endpoint)
- Claude Code, OpenCode and the
DeepSeek Harness, chosen per session
— the protocol is agent-neutral, so another agent is one driver away
(
docs/PROTOCOL.md) - File attachments (photos or any file), and project/folder management on every paired bridge
- NIP-42
AUTHrelays, and Tor on both ends (see below)
CodeDeck+ is a community-maintained continuation of CodeDeck Next by JeroenOnNostr (mobile · bridge), consolidated into one repository and since rebuilt: the protocol, the bridge and the phone's core are Rust, and the Android app is native. It adds infrastructure the upstream projects didn't design for:
- NIP-42
AUTHrelays — e.g. a self-hosted Haven relay - the bridge reaching its relays only over Tor (SOCKS5)
- the phone routing through Orbot (Android's Tor app)
The original MIT license and attribution are preserved. Upstream is not tracked mechanically any more: the protocol and both halves have been rewritten, so ideas from upstream are re-implemented here as ordinary changes.
codedeck-plus/
├── crates/ # the Rust workspace
│ ├── protocol/ # the phone wire (source of truth) + its conformance corpus
│ ├── agent-protocol/ # the driver protocol: bridge ⇄ agent host
│ ├── nostr-transport/ # relay WebSocket + SOCKS5 (Tor) driver, NIP-42 AUTH
│ ├── bridge-core/ # the bridge engine (pure state machine)
│ ├── bridge-runtime/ # the codedeck-bridge binary
│ ├── client-core/ # the phone's core (pure)
│ ├── client-runtime/ # the phone's async host
│ └── client-ffi/ # UniFFI surface for the Android app
├── packages/
│ └── agent-host/ # Node sidecar running the agent SDKs (one driver per agent)
├── apps/
│ ├── android/ # the native Android app (Kotlin + the Rust client core)
│ └── mobile/ # the former Tauri app — frozen (future desktop client)
├── docker/ # the bridge image (Dockerfile, entrypoint, helpers)
├── deploy/ # systemd unit for the bridge
├── docs/ # PROTOCOL.md (contract) · BRIDGE.md · CLIENT.md · OPENCODE.md
├── scripts/ # toolchain installer (Linux), shared shell helpers
├── .github/workflows/ # ci.yml · release.yml (tag → release)
├── .claude/ # CLAUDE.md + skills for Claude Code
├── codedeck # ./codedeck — bridge / Tor / APK / checks wrapper
├── docker-compose.yml
└── data/ # runtime volume (bridge identity, paired phones, sessions)
- NIP-42 relay auth (
crates/protocol/src/nip42.rs): the bridge and the phone each answer a relay'sAUTHchallenge with their own existing identity keypair — no new secret to configure. Just allowlist the bridge's pairing npub (and the phone's, if your relay gates reads too) in your relay's ACL. - Tor/SOCKS5 for the bridge (
crates/nostr-transport): settorProxyUrlin itsconfig.json(orCODEDECK_TOR_PROXY_URL) and every relay connection routes through it (the agents' own API traffic does not). See the optionalcodedeck-torCompose service below. - Orbot for the phone: a settings toggle routes the app's relay and image traffic through Orbot's SOCKS5 proxy — off by default.
Other ways to run it (release archives, systemd, from source) are in
docs/BRIDGE.md.
Two files configure it:
-
.env(copy.env.example) — what the container needs: the Claude Code and GitHub tokens, the Git identity and repositories to clone, and a few optional switches.CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat... GITHUB_TOKEN=ghp_... GIT_USER=your_username GIT_EMAIL=your_email@example.com # Comma-separated repositories; cloned under /data/workspaces/<repo-name> GIT_REPO=https://github.com/your-username/your-repo.git
-
data/config.json— the bridge's own settings: relays, Tor, OpenCode, the direct link, the machine name. The first start creates it with the direct link on and the defaults for everything else;config.example.jsonshows every setting (keep only the ones you need), anddocs/BRIDGE.mdexplains each. Restart the container after editing it.The most common of them can be set in
.envinstead (CODEDECK_RELAYS,CODEDECK_TOR_PROXY_URL, the OpenCode and direct-link ones — see.env.example). Use whichever you prefer: a variable set in.envwins overconfig.json, and one left unset leaves the file's value in place.{ "relays": ["wss://relay.example.com"], "direct": { "listen": "0.0.0.0:7447" } }
The direct link is on out of the box: Compose publishes port 7447, and phones
on your network or VPN reach the bridge there without a relay (only paired
phones get past its handshake). Inside a container the bridge cannot see the
host's address, so add it once, either in config.json
("endpoints": ["wss://192.168.1.20:7447"] in direct) or on the phone, in
the machine's settings. Either works over wss:// with no certificate setup:
the phone pins the certificate the bridge reports, not a host name.
On a server with a public address, the published port is reachable from the
internet too. Only paired phones get past the handshake, but if you would
rather not expose it, drop the ports: entry or bind it to your VPN address
("100.64.0.1:7447:7447").
Docker Compose reads the root .env file as its environment configuration. It
maps CLAUDE_CODE_OAUTH_TOKEN and GITHUB_TOKEN from that environment into
Docker secrets, mounted in the container at:
/run/secrets/claude_code_oauth_token/run/secrets/github_token
The bridge exports CLAUDE_CODE_OAUTH_TOKEN because Claude Code needs it. It
does not export GITHUB_TOKEN into the bridge or Claude environment: Git reads
the GitHub credential through git-credential-codedeck-secret, and gh reads
it through gh-codedeck-secret. Keep real values only in your untracked
.env file, use least-privilege tokens, and rotate them if they appear in
logs or source control.
- Build and start the container:
docker compose up -d --build- Optional — also run the bundled Tor daemon (
lncm/tor), instead of pointingtorProxyUrlat a Tor daemon you already run elsewhere:
docker compose --profile tor up -d --build
# then set "torProxyUrl": "socks5h://codedeck-tor:9050" in data/config.json
# (or CODEDECK_TOR_PROXY_URL in .env)- Check the logs to scan the pairing QR code with the CodeDeck+ Android app:
docker compose logs -f codedeck-bridgePushing a vMAJOR.MINOR.PATCH tag runs .github/workflows/release.yml,
which publishes one GitHub Release with every artifact of that version:
| Component | Artifact |
|---|---|
| Android app | codedeck-vX.Y.Z.apk — release aarch64 build, signed |
| Bridge for Linux (binary + agent host + Node; glibc ≥ 2.35) | codedeck-bridge-vX.Y.Z-linux-x86_64.tar.xz, …-linux-aarch64.tar.xz |
| Bridge for Windows (the same, zipped) | codedeck-bridge-vX.Y.Z-windows-x86_64.zip |
| Bridge container image | ghcr.io/deymosh/codedeck-plus-bridge:vX.Y.Z (and :latest) |
The archives and the image leave out the agents' own binaries and install
them on first use, pinned to the lockfile (see docs/BRIDGE.md);
CODEDECK_BUNDLE_AGENTS=1 builds an image that carries them.
A tag with a hyphen (v1.2.3-rc1) is published as a prerelease and does not move
:latest. The version is bumped in the tree in the commit that gets tagged (one
number for the whole monorepo); workflow_dispatch runs the same pipeline for a
dry run.
APK signing needs four repository secrets — SIGNING_KEY (base64 keystore),
KEY_ALIAS, KEY_STORE_PASSWORD, KEY_PASSWORD. See
.claude/skills/cut-release/SKILL.md for
the full runbook.
docs/BRIDGE.md— the bridge: install, commands, config, files, systemddocs/CLIENT.md— the phone: the Rust client core, the Android app, transport rules, building the APKdocs/PROTOCOL.md— the v11 wire contract, the driver protocol, adding an agentdocs/OPENCODE.md— the optional OpenCode session backend: external server vs. bridge-managed, config, Docker setupdocs/DEEPSEEK.md— the DeepSeek Harness backend: API key, models and reasoning, gateways, MCP servers and pluginsdocs/AGENT-CANDIDATES.md— which agents to add next and how, and installing agents on demand.claude/skills/cut-release/SKILL.md— the release runbook
- codedeck-next-mobile — upstream Android app
- codedeck-next-bridge — upstream headless CLI / VPS bridge