A small Rust CLI that bootstraps short-lived cloud development boxes for agentic coding sessions. Zero-config by default — kleya launch provisions a spot / preemptible instance, runs an embedded bootstrap that installs the usual agent toolchain (zsh, oh-my-zsh, tmux, git, rust, node, jj, python, uv, Claude Code), and prints an ssh invocation to attach.
Status: v0.1.0-rc.4 prerelease. Unix only (Linux + macOS, x86_64 + aarch64). Windows is out of scope.
Provider support. kleya is built around a provider-neutral CloudCompute port in kleya-core. Cloud-specific code lives in adapter crates (kleya-aws, and any future siblings) that depend only on kleya-core — adding a new provider is a new crate, not a refactor of the binary. The only adapter shipped in v0.1 is AWS EC2 (Amazon Linux 2023, ARM or x86); the design and tradeoffs of the port are documented in docs/specs/04-provider-port.md. The rest of this README assumes the AWS adapter; sections that are adapter-specific are marked accordingly.
Always required:
- The
sshbinary on yourPATH(kleyaexecs it forconnect). tmuxon the remote instance (installed by the bootstrap script).
AWS adapter (the only provider in v0.1):
- An AWS account with:
- Working credentials reachable through the SDK default chain (env vars, profile, IAM Identity Center cached token,
aws loginconsole credentials,credential_processhelper, web-identity OIDC, or IMDS — seedocs/specs/11-credentials-and-sso.md). Authentication happens outside kleya (aws sso login,aws login,aws configure, etc.). - A default VPC in the region you launch into.
- Permission to call
ec2:*for templates, instances, security groups, key pairs, andssm:GetParameterfor the AL2023 AMI alias.
- Working credentials reachable through the SDK default chain (env vars, profile, IAM Identity Center cached token,
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/antstanley/kleya/releases/download/v0.1.0-rc.4/kleya-cli-installer.sh | shInstalls kleya to ~/.cargo/bin/ (or ~/.local/bin/, whichever is on your PATH).
Grab the right tarball from the releases page for your target:
kleya-cli-x86_64-unknown-linux-gnu.tar.xzkleya-cli-aarch64-unknown-linux-gnu.tar.xzkleya-cli-x86_64-apple-darwin.tar.xzkleya-cli-aarch64-apple-darwin.tar.xz
Each tarball contains a single kleya binary. A matching .sha256 checksum file is published alongside each tarball; sha256.sum aggregates all of them.
git clone https://github.com/antstanley/kleya.git
cd kleya
cargo install --path crates/kleya-cli --lockedRequires the workspace toolchain (Rust 1.95.0; rust-toolchain.toml pins it).
# 1. Launch a zero-config spot instance and wait for it to be reachable
kleya launch --connect
# 2. Once you're in, work happens on the remote box. Detach with Ctrl-b d.
# 3. Re-attach from another terminal
kleya connect <name> # the name printed at launch
# 4. List your kleya-managed instances
kleya list
# 5. Tear it down
kleya terminate <name>kleya launch --connect waits for SSH to come up, waits for cloud-init to finish (so you land on a fully bootstrapped box), and then execs ssh + tmux new-session -A. The named tmux session means subsequent kleya connect <name> calls reattach to the same session.
The CLI surface is provider-neutral by intent — every subcommand maps onto a method on CloudCompute. The concrete defaults (m8g.xlarge instance type, amazon-linux-2023-arm64 AMI alias, etc.) are AWS-shaped because AWS is the only adapter in v0.1; an additional provider would resolve them from its own catalog.
Launches a spot (default) instance from a template.
kleya launch [--template <name>] [--name <name>]
[--instance-type <type>] [--market spot|on-demand]
[--connect] [--wait-bootstrap] [--no-wait-bootstrap]
[--dry-run]
| Flag | Meaning |
|---|---|
--template <name> |
Use a template defined in your config (default: an auto-created default template) |
--name <name> |
Tag the instance with this human-readable name. Auto-generated if omitted (kleya-<adj>-<animal>) |
--instance-type <t> |
Override the template's instance type (e.g. m7g.xlarge, t4g.medium) |
--market spot|on-demand |
Override the template's market |
--connect |
After SSH is reachable, wait for cloud-init and exec ssh. Implies --wait-bootstrap unless overridden |
--wait-bootstrap |
Run cloud-init status --wait over SSH before returning |
--no-wait-bootstrap |
Opt out of the cloud-init wait when --connect is set (TCP-22 only) |
--dry-run |
Resolve and print the launch plan; exit 0 without provisioning |
Lists instances tagged kleya:managed=true.
kleya list [--json]
Resolves a handle (name or a provider-native instance id, e.g. i-… for AWS) to a managed instance, looks up the right private key via the kleya:key tag, and execs ssh.
kleya connect <name> [--print] [--no-tmux] [--tmux-session <s>] [--instance-id <id>]
| Flag | Meaning |
|---|---|
--print |
Print the ssh argv and exit; don't actually connect |
--no-tmux |
Skip the tmux new-session -A wrapper |
--tmux-session <s> |
Override the configured tmux session name |
--instance-id <id> |
Resolve by the provider's native instance id instead of name. AWS adapter accepts i-…. Useful for unmanaged instances. |
kleya terminate <name> [--yes]
Confirms interactively unless --yes is passed.
Templates capture provider-specific launch configuration so kleya launch --template <n> is a one-shot. On the AWS adapter these map to EC2 Launch Templates.
kleya template create --name <n> [--ami ami-...] [--instance-type <t>] [--key-name <k>] [--user-data <path>]
kleya template update --name <n> [...same flags...]
kleya template list [--json]
kleya template delete <name> [--yes]
The --ami flag is AWS-adapter-specific; other adapters expose their own equivalent (machine image, machine type, project / zone, …).
kleya config show # print the merged config (defaults + file + flags) as TOML
kleya config path # print the path of the file that was actually loaded (or "<defaults>")
If you drive kleya from a coding agent (Claude Code, Cursor, OpenCode, Codex), you can install a companion skill that teaches the agent the launch / connect / templates / unattended-handoff workflows so it stops reaching for raw aws ec2 commands.
curl --proto '=https' --tlsv1.2 -fsSL \
https://github.com/antstanley/kleya/releases/download/v0.1.0-rc.4/install-skill.sh | shThe installer autodetects which agents you have configured (it looks for ~/.claude/, ~/.cursor/, ~/.config/opencode/, ~/.agents/, ~/.codex/) and writes the skill to each one's native location. Override with --target=claude,opencode (comma-separated) or --target=all.
~/.claude/skills/using-kleya/SKILL.md— Claude Code~/.cursor/skills/using-kleya/SKILL.md— Cursor (also picks up the Claude path)~/.config/opencode/skills/using-kleya/SKILL.md— OpenCode~/.agents/skills/using-kleya/SKILL.md—.agentsfolder spec (cross-agent fallback)~/.codex/AGENTS.md— Codex (appended between idempotent marker comments)
Restart your agent after install. To pin a specific version, pass --version=v0.1.0-rc.4.
Optional. kleya launch with no flags and no config file launches a working dev box.
Resolved in this order:
--config <path>flagKLEYA_CONFIGenvironment variable~/.config/kleya/config.toml(or.yaml,.json,.jsonc)
TOML, YAML, JSON, and JSONC are all accepted. Format is detected from the extension.
Field names marked (AWS adapter) are interpreted by the AWS adapter; other adapters will accept their own equivalents.
default_region = "eu-west-1" # AWS region for the AWS adapter
default_profile = "default" # AWS named profile (AWS adapter)
[defaults]
instance_type = "m8g.xlarge"
market = "spot" # or "on-demand"
spot_type = "one-time" # or "persistent"
ami_alias = "amazon-linux-2023-arm64" # AWS adapter — SSM-resolved at launch
[bootstrap]
# Optional path to a custom user-data script. If set, install_ghostty_terminfo
# has no effect (the script is passed through verbatim, base64-encoded).
# user_data_path = "~/.config/kleya/bootstrap.sh"
install_ghostty_terminfo = true
[ssh]
user = "ec2-user"
tmux = true
tmux_session = "kleya"
term = "xterm-256color" # TERM sent to the remote pty; "" sends local $TERM
extra_args = [] # appended verbatim to the ssh argv
[keys]
dir = "~/.config/kleya/keys"
default_key_name = "kleya-default"
# Optional per-template overrides. Templates are created on demand by
# `kleya template create`, or implicitly by `kleya launch --template <name>`.
[[templates]]
name = "gpu"
instance_type = "g6.xlarge"
# ami_id = "ami-..." # optional; otherwise resolved from ami_alias
# key_name = "..."
# security_group_ids = ["sg-..."]
# subnet_id = "subnet-..."
[[templates.tags]]
key = "Project"
value = "gpu-experiments"| Variable | Effect |
|---|---|
KLEYA_CONFIG |
Path to config file (overrides default search) |
KLEYA_PROFILE |
Provider profile to use (overrides default_profile). On the AWS adapter, this is the AWS named profile. |
KLEYA_REGION |
Provider region (overrides default_region). AWS adapter passes this to the SDK. |
KLEYA_LOG_FORMAT |
text (default) or json |
AWS_* |
Standard AWS SDK env vars (AWS_ACCESS_KEY_ID, AWS_PROFILE, …) — honoured by the SDK itself; kleya does not read them directly. |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | User-data exceeds limits |
| 2 | Config invalid / pre-flight check failed |
| 3 | Instance not found |
| 4 | Handle ambiguous (multiple instances match the name) |
| 5 | SSH not ready within timeout (default 180s) |
| 6 | Launch wait timed out (default 600s) |
| 7 | Key mismatch (local fingerprint differs from the provider's record) or key orphaned (provider has the key, local pem missing) |
| 70 | Provider adapter error (SDK / network / API). On the AWS adapter this includes anything aws-sdk-ec2 / aws-sdk-ssm surfaces. |
| 74 | I/O error |
| 130 | Cancelled (Ctrl-C / SIGINT) |
Most adapter-specific failures surface as Error::Adapter { provider, source } (exit code 70) with the provider's own error in the source field. The list below is for AWS-adapter-specific cases that have a known kleya-side remediation.
Adapter aws-ec2: ... no default VPC (AWS adapter). kleya's zero-config path assumes a default VPC in the chosen region. Either create one (aws ec2 create-default-vpc) or specify subnet_id and security_group_ids in a [[templates]] block.
KeyMismatch. Your local ~/.config/kleya/keys/<name>.pem fingerprint differs from what the provider has registered. Either delete the local pem (kleya will treat as orphaned and you can re-import the provider's public half manually), or remove the provider-side key and let kleya regenerate on next launch (kleya launch --regenerate-key).
SshNotReady after 180s. The probe couldn't reach port 22. Check the provider's firewall / security group allows your IP and that the instance actually started (kleya list or the provider console).
launch --connect drops you mid-bootstrap. Use --wait-bootstrap (the default when --connect is set unless --no-wait-bootstrap is also passed). The wait runs cloud-init status --wait over SSH before returning control.
The full design lives in docs/specs/ — eleven numbered pages indexed by docs/README.md. Start with docs/specs/00-overview.md. The pages most relevant to extending kleya:
04-provider-port.md— theCloudComputetrait, idempotency contract, and what a new adapter has to implement.06-launch-and-connect.md— launch orchestration, key lifecycle, SSH probe.11-credentials-and-sso.md— credentials chain, profile / region resolution, and why kleya never owns login.
Dual-licensed under Apache-2.0 OR MIT per Cargo.toml. You may use this crate under the terms of either, at your option.
See CONTRIBUTING.md for repo layout, tooling, conventions, and the release process. The canonical design spec at docs/specs/ is the source of truth for non-trivial changes.