- Rust 89.1%
- Shell 10.1%
- Dockerfile 0.8%
|
All checks were successful
ci / images (., images/gateway/Containerfile, gateway) (push) Successful in 3m30s
ci / chart (push) Successful in 22s
ci / images (., images/cli/Containerfile, cli) (push) Successful in 2m35s
ci / rust (push) Successful in 1m36s
ci / runner-image (push) Successful in 2m10s
ci / images (., images/controller/Containerfile, controller) (push) Successful in 3m50s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| crates | ||
| deploy/chart | ||
| docs | ||
| images | ||
| scripts | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
| rust-toolchain.toml | ||
Claudify
Collaborative Claude Code multiplexer. A small team runs one Claudify deployment on Kubernetes; team members attach from their laptops to per-session pods that hold a long-running Claude Code instance plus the project worktree. Credentials, project repos, and runtime live in the cluster; the client is thin (one SSH connection for the PTY, one HTTPS bearer for the control plane).
Status: v0.6.x — actively used internally. CLI is stable; CRDs are versioned v1alpha1. See docs/deployment-readiness.md for what's proven end-to-end.
What it does
- One session pod per session. Claude Code runs inside
dtach, a single-PTY detach/reattach wrapper. Multiple teammates can attach concurrently and share one canvas; killing the SSH connection leaves Claude running. - Projects, accounts, settings as CRDs. A project registers a git repo, an account holds per-engineer Claude OAuth + git SSH + Forgejo MCP credentials, a settings CR carries shared dotfiles/agents/plugins/
mcp.json. Sessions pick one of each at create time. - Per-session resource limits. Each session pod has RAM/CPU caps so one runaway agent can't starve the cluster.
- SSH pubkey auth. A flat
claudify-trusted-keysConfigMap lists all allowed clients; anyone listed can do anything. Revocation = remove the key. - HTTPS control plane. Login signs a challenge with the user's SSH key (SSHSIG); the server returns a 15-minute bearer used for the rest of the session.
Install the CLI
macOS / Linux via Homebrew (recommended)
brew install public/brew-tap/claudify
Shell completions for fish / bash / zsh are installed automatically.
From source
cargo install --git ssh://git@code.podesta.ai/qwolff/claudify.git claudify-cli
# or, from a clone:
git clone ssh://git@code.podesta.ai/qwolff/claudify.git && cd claudify
cargo install --path crates/cli
cargo install does not install shell completions automatically — generate them:
# fish (current shell): claudify completions fish | source
# fish (persistent): claudify completions fish > ~/.config/fish/completions/claudify.fish
# bash: claudify completions bash > /usr/local/etc/bash_completion.d/claudify
# zsh: claudify completions zsh > "${fpath[1]}/_claudify"
First-run setup
claudify setup
Two questions, both with defaults — hit Enter to accept. The command writes ~/.config/claudify/config.toml, runs a login (signs a challenge with your SSH key), and verifies access with a list-sessions probe. After it succeeds:
claudify status # cluster health overview
claudify list # your sessions
claudify create --project PROJECT --account ACCOUNT --settings SETTINGS --name NAME --attach
--yes accepts all defaults non-interactively (for dev-box bootstrapping); --skip-login saves the config but doesn't try to talk to the gateway (useful when configuring before a port-forward is up).
Env vars override the config file for one-off invocations:
| Variable | Default |
|---|---|
CLAUDIFY_API |
https://claudify.tu-po.com |
CLAUDIFY_SSH_HOST |
host part of CLAUDIFY_API |
CLAUDIFY_SSH_PORT |
2222 |
CLAUDIFY_KEY |
auto-detect ~/.ssh/id_ed25519, then id_rsa |
CLAUDIFY_KEY picks the private key for everything claudify does over SSH: login's SSHSIG challenge signature and the actual ssh connections attach/shell/exec/cp/teleport open (passed as -i <key> -o IdentitiesOnly=yes, so ssh can't silently authenticate as some other key from your agent instead). Set it whenever you have more than one local key, so the key that signs the login challenge is guaranteed to be the same one the SSH connections authenticate with. If unset, login auto-detects ~/.ssh/id_ed25519, then id_rsa; the ssh connections fall back to ssh's own default identity discovery, which isn't guaranteed to pick the same key.
Command reference
| Command | What it does |
|---|---|
claudify setup |
First-run interactive config + login |
claudify login |
Re-sign a fresh bearer (auto on 401) |
claudify status |
Cluster health + resource counts (cached, fast) |
claudify list (ls, ps) |
List sessions with phase / project / attach count |
claudify top |
Live cpu/memory per session (5s refresh) |
claudify create --project P --account A --settings S --name N [--attach] |
New session; --attach auto-attaches once Running |
claudify attach <sess> |
Attach to the long-running Claude PTY |
claudify shell <sess> (sh, bash) |
Fresh bash -l in the same pod — escape hatch from Claude |
claudify exec <sess> -- <cmd> (run) |
One-shot non-interactive command; pipes work, exit code propagates |
claudify cp [-r] <src>… <sess>:<dest> (and the reverse) |
Copy files to/from a session pod over the claudify-cp SSH subsystem (tar|zstd stream) — the only way to get a file into a pod, since the gateway has no exec channel or sftp for scp/rsync |
claudify teleport [--name N --project P --account A --settings S] [--with-artifacts] |
Move the repo you're standing in onto a fresh session: unpushed commits, uncommitted/untracked files, allowlisted secrets, and the Claude conversation about it — resumed, not started fresh |
claudify logs <sess> [--tail N --previous] |
Pod kubelet logs |
claudify end <sess…> (rm, stop, delete) |
Delete session(s); idempotent, accepts multiple names |
claudify project {list,get,apply,rm} |
Manage project CRs (the git-repo defs) |
claudify account {list,get,apply,rm} |
Manage account CRs (Claude+git+fgj creds) |
claudify settings {list,get,apply,rm} |
Manage settings CRs (shared dotfiles/agents) |
claudify whoami |
Current bearer's key fingerprint |
claudify completions {fish,bash,zsh} |
Print a shell completion script |
All session-name positional args TAB-complete from a live name list (5s cache; the lookup runs in well under 300 ms over WAN). Same for --project, --account, --settings flag values.
See docs/cli.md for per-command details (flag matrix, examples, exit codes).
For operators (deploying the cluster side)
See docs/setup.md. Short version:
- Helm-install the chart pointing at a StorageClass on your cluster (
--set rwx.storageClassName=...). - Generate + install the gateway's SSH host key as a Secret (or let the chart auto-generate on first install).
- Add at least one SSH pubkey to
claudify-trusted-keysConfigMap. - Create projects / accounts / settings via the CLI.
Repo layout
claudify/
├── crates/
│ ├── api/ # CRD types, HTTP wire types, SSHSIG canonical payload
│ ├── controller/ # reconciles ClaudifyProject/Account/Settings/Session CRDs
│ ├── gateway/ # SSH server (:2222) + HTTPS control plane (:8443)
│ ├── cli/ # `claudify` CLI
│ └── integration-tests/ # kind-based smoke scenarios (#[ignore] by default)
├── images/
│ ├── controller/ # distroless, static musl
│ ├── gateway/ # distroless, static musl
│ ├── cli/ # distroless, static musl
│ └── runner/ # session pod image (extends public/runner-image)
├── deploy/chart/ # Helm chart
└── docs/
├── setup.md # operator install guide
├── cli.md # full CLI reference
├── architecture.md # one-page architecture
└── deployment-readiness.md
Development
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all --check
helm lint deploy/chart
helm template claudify deploy/chart > /tmp/render.yaml # render to inspect
Integration tests against a real kind cluster need kind, helm, kubectl, podman on PATH and run with --ignored:
cargo test -p claudify-integration-tests -- --ignored --test-threads=1
Not run in CI (runners are unprivileged; kind needs nested container runtime).
Releasing
Single semver across the whole monorepo. To cut a release:
- Bump
version = "X.Y.Z"in allcrates/*/Cargo.toml, runcargo update -p claudify-* …, commit asrelease: X.Y.Z — <one-line>. - Tag
vX.Y.Z, push commit + tag. - CI builds + pushes the gateway/controller/cli/runner images and the Helm chart on tag.
- Bump the brew formula in
public/brew-tap.
Known limitations
- Multi-attach pair-programming. Concurrent keyboard input interleaves byte-by-byte; use voice to coordinate, or spin separate sessions.
- Credential rotation requires recreating sessions. Updating an account/settings Secret does not propagate into running pods.
- Trusting-group threat model. Any user with a trusted key can read every Secret the gateway can read, create sessions under any account, and exec into any session pod. Don't deploy as multi-tenant SaaS.
License
Apache-2.0.