Skip to content

OpenCharly

The wonky DevOps tool for you and your agents.

charly is a command-line tool that orchestrates one declarative description of a working environment onto any of five substrates: a container, a VM guest, a Kubernetes cluster, a host, or an Android device. You write a list of candies — each one an installable concern — and charly realises the same list everywhere, for you and for the agents you run. Every substrate consumes the same intermediate form, the install plan, so moving a deploy from a container to a VM is a keyword change rather than a rewrite.

charly brings everything and nothing. Everything you might want can be added as a plugin — a candy — and nothing is built in that you cannot trim out. If you would rather use a different tool for a particular job, remove the candy that provides it and wire yours in; the orchestrator does not care what provides a concern, only that the install plan is satisfied.

# everything — compose the kitchen sink
dev-box:
candy:
base: fedora
candy:
- '@github.com/opencharly/layer-ripgrep:v2026.235.1653'
- '@github.com/opencharly/pod-sshd:v2026.239.1637'
- '@github.com/opencharly/layer-charly:v2026.241.1407'
# nothing — the same box, trimmed to one concern; bring your own tool for the rest
minimal-box:
candy:
base: fedora
candy:
- '@github.com/opencharly/pod-sshd:v2026.239.1637'

Every word charly understands is itself a plugin — every word is a plugin — so the trim is not a special case: the core is word-blind, and a candy list is the whole configuration.

DevOps’ oldest trick is to bring the pain forward: take the hard, risky part of shipping — the deployment, the integration, the teardown — and do it early and often, on a system built to be destroyed, so it stops being hard by the time it matters. charly is built around that mantra.

Every candy carries a plan: — an acceptance spec that proves what it installs, baked into the image as an OCI label. Every deploy marked disposable: true is a check bed: a test system that exists to be destroyed. charly check run <bed> chains build, deploy, probe, destroy, rebuild, and probe again in one command — the whole pain of shipping, brought forward to the test system, on every change, before anything reaches a real target.

The same install plan the bed just proved is the one a production deploy realises — so the pain you bring forward is the pain you would otherwise meet in production, and by the time it gets there, it is routine.

Heritage. The design borrows deliberately from LLVM: one intermediate representation, many backends — applied to infrastructure deployment instead of code generation, and driven by agents as readily as by people. That spine is what makes the orchestration target-neutral.

A candy is the atomic unit of configuration; a box is a candy that composes others into a buildable machine. The names are literal — candy: is a real keyword and candy/ is a real directory.

A box is an artifact; a candybox is a process. Nothing about a box’s contents makes it safe — a box is deliberately generous, full shell and package manager included. The isolation is a property of the running container or VM — the candybox. That is why it is safe to hand an agent everything inside one, and why disposable: true is a statement about a running thing rather than about a file.

One box can be deployed many times, onto several substrates, on several machines; each of those is a deploy, and the deploys on one machine are that machine’s deploy.

Full documentation: opencharly.ai.


Install charly once, then use it from anywhere. charly runs on Linux; container deploys need Podman or Docker, VM deploys need libvirt. Everything else here — and every page on opencharly.ai — assumes a machine with charly installed.

How to install →

Once charly is on your $PATH, --repo reads any published project without cloning it:

Terminal window
charly --repo opencharly/charly box list boxes

The first lines of its output:

agentteams [testing]
agentteams-manager [testing]
agentteams-worker [testing]
alpine-repo-box [testing]
arch.arch [testing]

Clone the repository only if you are working on charly itself:

Terminal window
git clone --recurse-submodules https://github.com/opencharly/charly.git
cd charly
task build:binary # builds ./bin/charly, stamped with a CalVer (date-based) version
./bin/charly box build

Every invocation against that checkout uses ./bin/charly; each checkout or worktree gets its own.


This is box/tutorial-shell/charly.yml, from the opencharly/distro-fedora project:

tutorial-shell:
candy:
description: |-
The teaching box behind opencharly.ai's quickstart — a minimal, real dev shell
...
base: fedora
candy:
- '@github.com/opencharly/layer-supervisord:v2026.240.0121'
- '@github.com/opencharly/layer-ripgrep:v2026.235.1653'
- '@github.com/opencharly/pod-sshd:v2026.239.1637'
plan:
- check: composing the service candy next to the init candy wired sshd into the assembled supervisord config — a program block neither candy produces on its own
id: tutorial-shell-service-wired-into-init
file:
file: /etc/supervisord.conf
contains:
- contains: "[program:sshd]"

base: points at another box defined next door; it can equally be a registry ref. The inner candy: is the list of candies the box composes.

Note what is listed: the init candy. sshd declares a service, so charly resolves whichever init the destination needs and composes that init’s candy — supervisord when this box is built as a container image, the systemd init instead when the same candies land on a systemd machine. The box names supervisord explicitly so the assembled /etc/supervisord.conf is deterministic: without the acting init’s candy in the scanned set charly warns and injects nothing.

The plan: does not check that ripgrep and sshd are present — each candy’s own plan proves that, and those plans run against this same image. It checks what the composition produced: that sshd became a supervisord program. A check belongs on the candy that provides the behaviour, and belongs on the composing box only when the claim is about the composition itself.

Terminal window
charly --repo opencharly/distro-fedora box validate # the schema gate — nothing runs until it passes
charly --repo opencharly/distro-fedora box build tutorial-shell # → multi-stage Containerfile → image
charly --repo opencharly/distro-fedora shell tutorial-shell # → you are inside the candybox
charly --repo opencharly/distro-fedora check run check-tutorial-shell # → build, deploy, probe, fresh rebuild, tear down
Stage What you write What you run
Build a candy: with base: and a candy list charly box build <box>
Run nothing more charly shell <box>
Deploy a substrate keyword — pod: vm: kubernetes: local: android: charly deploy add, charly start
Evaluate a plan: on each candy charly check box, charly check live, charly check run

A deploy marked disposable: true is a check bed, and charly check run <bed> chains build, deploy and evaluate into one command. It builds the image and checks it, deploys it, and waits for steady state. Then it checks the running candybox live, destroys and rebuilds it from scratch, checks it again, and tears everything down.

Both stanzas below are real entries in box/fedora/charly.yml: this page’s example as a container bed, and its VM twin.

# a CONTAINER
check-tutorial-shell:
pod:
image: tutorial-shell
disposable: true
# a VM GUEST
check-fedora-vm:
vm:
from: fedora-vm
disposable: true
add_candy:
- '@github.com/opencharly/layer-charly:v2026.241.1407'

The payloads differ — the pod runs the built tutorial-shell image; the VM boots the fedora-vm template (from: inherits its settings) and overlays the charly candy. The grammar does not: the substrate is the keyword, pod: versus vm:, and each candy a deploy names is realised through the same install plan on either side — the VM stanza reaches its guest over SSH and installs packages there, the pod stanza runs an image. kubernetes:, local: and android: take the same shape — how that is wired →

The teaching box above has a small candy list. The kitchen-sink dev boxes are fedora-coder, arch-coder, debian-coder and ubuntu-coder. Each carries the AI coding CLIs (claude-code, codex, gemini, forgecode), language runtimes, DevOps tooling, and nested rootless containers and VMs — all at uid 1000 with no --privileged. Same format, same commands. Because those containers and VMs are nested, a candybox can build and deploy candyboxes — charly runs inside charly.


A candy is declared with one entity keyword, candy:, and one filename, charly.yml. But a candy resolves to one of two shapes, and base:/from: is the switch:

The candy carries It is And it may also carry
neither base: nor from: a layer — one installable concern package: service: plan:, and a plugin: block
base: or from: a box — a candy that composes other candies; box build turns it into a container image a candy: list of layers, plan:

The two shapes are mutually exclusive, and the schema enforces it — a candy that carries both base: and package: is rejected. A box does not install packages directly; it composes layers that do.

Every provider word in this document is registered by a plugin candy. Many of them are ordinary layers as well — every one carrying a plan:, so each is a real layer and an extension of charly at the same time. charly is not a program with built-in support for containers, VMs and Kubernetes that also happens to accept plugins. Its core is word-blind: it loads plugins and routes each word to whichever one claims it. It does not know what pod: means — there is no pod case in a switch statement in the core.

The provider index is the live census — every word and its owning plugin candy, regenerated on every docs build.

Role Examples
substrate — deploy destinations (the census registers these as the kind and deploy classes) pod vm kubernetes local android
kind — the entity keywords themselves candy distro group agent
verb — probes a plan: can call file http cdp vnc adb kube
commandcharly subcommands deploy check clean marketplace
step — install operations service-custom reboot
builder — multi-stage build patterns pixi npm cargo aur

candy: itself is a plugin-provided kind, registered by candy/plugin-candy-kind.

You extend charly by writing candies — in this project or any other, referenced by git URL — and a substrate you invent is the same kind of thing as pod:. This is the everything-and-nothing strategy in action: the whole surface is pluggable, and the core ships nothing you cannot replace.

1. Everything starts as one resolved project. charly reads every charly.yml it discovers, follows @github.com/... references to other repositories, and resolves each entity to a pinned CalVer version. The result is a single in-memory project — your candies plus everyone else’s, flattened, with no notion yet of building or deploying. A candy reference is a git URL and a version, so a box can compose someone else’s candy without vendoring it; charly box reconcile keeps the pins aligned.

2. A plugin is reached the same way wherever it lives. Some plugin candies are compiled into the binary; the rest are loaded from a project’s candy/ directory and run as separate processes over gRPC. Both implement one Provider contract, so where a plugin runs is an operational choice — startup cost against isolation — not an API difference. All five substrates — pod:, vm:, local:, kubernetes:, android: — are out-of-process.

3. Building and deploying share one input. Building renders a multi-stage Containerfile from your candy list. Deploying reduces the same list to the install plan, which each substrate backend realises its own way: an SSH session against a VM guest, a Kustomize tree, packages on a host, an APK install — and, on pod:, the very image the build path produced.

4. The artifact carries its own description. What a box provides and how to prove it — its declared capabilities and its acceptance plan — are written into the image as ai.opencharly.* labels, so a pulled image can be inspected and tested by a machine that has never seen the source.

5. The schema is upstream of the code. The base grammar of charly.yml is CUE, a typed configuration language, in the spec module (github.com/opencharly/spec); each plugin ships its own CUE schema for the words it registers. The Go wire types are generated from that base schema, and load-time validation runs against the same embedded schema — a grammar change cannot reach the code without going through the module both sides consume.

Candies compose a box, at authoring time. Nesting places a deploy, at run time. A candy is never “inside” another candy; a deploy can be inside another deploy.

There is no nested: field you write. Nesting is position in the file — indent one deploy under another and the inner one runs inside the outer one’s candybox:

check-group:
vm:
from: eval-vm
disposable: true
...
check-group-member:
local:
from: check-group-app

That is a real entry in this repository’s charly.yml, abridged — charly migrate unrolled the former targetless group: node into the vm: primary above (the group scalars move onto it). The inner local: carries no host: field, and that is the point: it runs inside the parent’s candybox rather than naming a machine of its own.

A top-level local: deploy installs packages and systemd units onto the machine charly is running on. The same deploy, nested under a disposable vm:, installs them into a throwaway guest instead. Either way it is reversible: a local: deploy records each step it applies in an install ledger, and charly deploy del <name> tears it back down.

Podman and Docker are both first-class, for building and for running. charly auto-detects what is installed; either can be pinned per host with CHARLY_BUILD_ENGINE, CHARLY_RUN_ENGINE or the runtime config.

On a host with Podman and systemd, a pod: deploy runs as user-level systemd quadlets. charly config generates one charly-<name>.container quadlet per deploy, carrying its ports, volumes, devices and security settings; systemd turns it into charly-<name>.service and starts it at boot.

Where Podman and systemd are not both present, the same deploy runs directly against the engine.


Full teaching glossary: the words.

Term What it is
candy the atomic unit of configuration in charly — the entity everything else is made of: boxes compose candies, deploys apply them, substrates realise them
layer a candy that installs one concern — carrying neither base: nor from:
base a field on a candy that names a starting image — another box or a registry ref. Carrying base: makes a candy a box
from a field with two uses. On a candy, from: builder:<word> selects a multi-stage build pattern and makes the candy a box. On a deploy, from: <name> inherits another same-kind deploy’s settings — this sense does not make anything a box
box a candy that composes other candies — the composite unit. A box names a starting point via base or from and stacks layers into a single entity that charly builds into an image or deploys onto a substrate
image the built artifact a box produces — the generic word for what box build yields, stored in an image store or registry
container image (= OCI image) an image in the OCI container format — the artifact a pod: or kubernetes: deploy runs as a container, named after the Open Container Initiative spec that defines it
candybox the running, isolated form of a box — a container or a VM guest. This is the security boundary
container the running OCI process — the candybox’s form on a pod: deploy. Not the image, not the box
substrate the destination kind a deploy lands on — pod: vm: kubernetes: local: android:. A substrate is a place, not an artifact: the image is the payload, the substrate is where it runs
pod the container substrate — a pod: deploy runs a box’s image as a container. Not a Kubernetes Pod (the kubernetes: backend emits real ones)
vm the guest substrate — a vm: deploy boots a vm image as a rootless libvirt guest, reached over SSH
vm image the bootable disk a vm: deploy boots — a cloud_image qcow2 or a bootc image. A different artifact from a container image
kubernetes the Kubernetes substrate — a kubernetes: deploy emits a Kustomize overlay
local the host substrate — a local: deploy installs onto the machine charly runs on, or onto a remote machine when it carries host
android the device substrate — an android: deploy installs APKs onto a device or emulator
host a field on a local: deploy naming the machine to install onto — host: local (or absent) is the machine charly runs on, host: <user@machine> is an SSH target
deploy a named placement of a box on a substrate. When running, its candybox is the live thing
deploy the set of deploys charly manages on this machine — the boxes deployed together, the way docker compose brings up a set of services. charly deploy add puts a deploy in it; charly deploy del reverses it
plugin a candy that teaches charly a new word — it carries a plugin: block registering the words it provides, each of which is a provider. A plugin lives in the layer shape, but its role is extending charly
provider a word a plugin registers, which routes to that plugin when charly sees it — a kind, verb, command, step, builder, or substrate
kind the class of a top-level name in a charly.yml — the entity keywords
group a kind that holds a set of deploys started and torn down together
verb a probe a plan: step can call — the check vocabulary
command a charly subcommand — the CLI vocabulary
step an install operation in a plan:
builder a multi-stage build pattern a box can select
plan the ordered acceptance spec a candy carries, baked into its image as an OCI label — distinct from the install plan
install plan the target-neutral form of what a deploy installs — produced once from a box’s candy list, then every substrate realises it its own way
check bed a deploy marked disposable: true — one whose candybox exists to be destroyed and rebuilt, which is what authorises charly to run an unattended full test cycle on it

charly serves its whole command tree over MCP — the Model Context Protocol, the open standard for exposing tools to an AI agent. It serves on either Streamable HTTP or stdio; an MCP client sees charly’s commands as ordinary tools.

mcp is itself an out-of-process command plugin, discovered from a project’s candy/plugin-mcp rather than compiled into the binary — so point charly at a project that supplies it:

Terminal window
charly --repo opencharly/plugin-mcp mcp serve

AGENTS.md is the complete, harness-neutral rulebook; CLAUDE.md is one adapter of it, not a separate source of truth. Supporting a new harness means adding an adapter, never porting the project.

opencharly/marketplace ships one skill — a packaged instruction set an agent loads — for every candy, box, command and contributor subsystem. It also ships reusable agents: executors that drive the charly check beds and return verbatim proof; enforcers that gate claims. It installs through the plugin manager of each supported harness — Claude Code, Codex CLI, Kimi Code and pi. The exact commands are in that repository’s README, which doubles as the full skill index.


Everything factual about a candy, box, plugin or verb is generated from the sources in this repository and published at opencharly.ai, so it cannot drift from the code the way a hand-maintained copy in this file would.

You want Go to
to build your first thing QuickstartAuthoring a candy
the vocabulary The words
the ideas, in order, with runnable examples The concepts tour — twelve short pages
every command and flag CLI reference + The charly CLI
every candy and box Candy reference · Box reference
“what implements cdp:?” Provider index
something is broken Troubleshooting
why the project looks like this The vision · What it is reacting to
dated history each repo’s CHANGELOG/, one file per CalVer version

MIT