Get started
From zero to a running Capa program in under five minutes. Capa is a Python 3.10+ package that transpiles .capa source into Python and executes it with a small runtime.
Install
Three paths. The one-line installer if you just want capa on your PATH with no fuss; the manual binary download if you prefer to verify the asset yourself; from source if you intend to contribute or want the test suite locally.
Option A: one-line installer (recommended)
The installer downloads the latest pre-built binary and drops it in ~/.local/bin/capa (Linux / macOS) or %LOCALAPPDATA%\capa\capa.exe (Windows). Open a new shell afterwards and capa --version should answer.
# Linux / macOS Apple Silicon
$ curl -fsSL https://github.com/nelsonduarte/capa-language/releases/latest/download/install.sh | bash
Optional: verify before you run. Both one-liners serve the script from the release channel (releases/latest/download/), so they track the last tagged release rather than every commit on main. If you would rather read the script before piping it to a shell, download it alongside its checksum from the same release, verify, inspect, then run:
# Linux / macOS
$ curl -LO https://github.com/nelsonduarte/capa-language/releases/latest/download/install.sh
$ curl -LO https://github.com/nelsonduarte/capa-language/releases/latest/download/install.sh.sha256
$ sha256sum -c install.sh.sha256
install.sh: OK
$ less install.sh # read it, then:
$ bash install.sh
# Windows (PowerShell)
PS> irm https://github.com/nelsonduarte/capa-language/releases/latest/download/install.ps1 -OutFile install.ps1
PS> (Get-FileHash install.ps1 -Algorithm SHA256).Hash.ToLower()
PS> irm https://github.com/nelsonduarte/capa-language/releases/latest/download/install.ps1.sha256 # compare the two hashes, then:
PS> Get-Content install.ps1 # read it, then:
PS> .\install.ps1
The script and its .sha256 come from the same release origin, so this catches accidental corruption or a tampered-blob-only attacker, not one who fully controls the release. For the strongest guarantee, install a specific tagged binary and check its GitHub build attestation with gh attestation verify out of band.
# Windows (PowerShell)
PS> irm https://github.com/nelsonduarte/capa-language/releases/latest/download/install.ps1 | iex
Override the install location with the INSTALL_DIR (bash) or CAPA_INSTALL_DIR (PowerShell) environment variable. The installer is idempotent: rerun it to upgrade to the latest release.
What the installer does. It is a small, readable script: pick the right asset for your OS and arch, curl / Invoke-WebRequest it, chmod +x, strip macOS Gatekeeper quarantine on Darwin, and add the install directory to your PATH on every system: the user PATH on Windows via [Environment]::SetEnvironmentVariable, the shell rc on Linux and macOS (unless CAPA_NO_MODIFY_PATH is set). No admin rights required; no system-wide changes; remove ~/.local/bin/capa or the PATH entry to uninstall.
PATH on Linux / macOS. The Linux and macOS installers now add ~/.local/bin to your PATH automatically, in parity with Windows. The installer detects your shell and appends the line to the matching rc file, marked with a comment so it is easy to spot: bash to ~/.bashrc, zsh to ~/.zshrc, fish to ~/.config/fish/config.fish, falling back to ~/.profile if the shell cannot be determined. It is idempotent, so rerunning the installer never duplicates the line, and if ~/.local/bin is already on your PATH it does nothing. The change takes effect in a new shell, or run source on the rc file to pick it up in the current one; the script runs in a subshell, so it cannot alter your live session.
If you would rather manage your PATH by hand, set CAPA_NO_MODIFY_PATH=1 before running the installer. It then only prints the line to add instead of editing any rc file. Use the opt-out path, or the manual install (Option B) below, and add the line yourself. Default shell rc by OS:
- Ubuntu / Debian (bash): a fresh login shell already picks up
~/.local/binvia the default~/.profile; an existing interactive terminal reads~/.bashrc. - macOS (zsh, default since Catalina): add the line to
~/.zshrc. - Fedora / Arch / others:
~/.bashrcfor bash,~/.zshrcfor zsh,~/.config/fish/config.fishfor fish.
# bash
$ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
$ source ~/.bashrc
# zsh (macOS default, modern Linux)
$ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
$ source ~/.zshrc
Option B: manual binary download
Each tagged release publishes a standalone binary built with PyInstaller that bundles the Capa compiler and a Python interpreter into one file. Download, run. No pip install, no Python required, no toolchain on the host. The language server is bundled too, so capa lsp works straight from the binary with no extra dependency.
Linux: the binary is a CLI tool, not a desktop app. If you download capa-linux-x86_64 through a browser and double-click it in GNOME Files / Nautilus, you will see "No application installed for Executable files". That is normal: graphical file managers on Linux do not launch unknown executables for security reasons, and capa is a terminal program anyway. Open a terminal in the download folder, chmod +x capa-linux-x86_64, and run it as shown below.
Pick your platform:
Linux
x86_64
$ curl -L -o capa \
https://github.com/nelsonduarte/capa-language/releases/latest/download/capa-linux-x86_64
$ chmod +x capa
$ ./capa --run hello.capa
To run capa from any directory, move it
to ~/.local/bin and ensure that
directory is on your PATH (see the
PATH callout under Option A above):
$ mkdir -p ~/.local/bin
$ mv capa ~/.local/bin/
$ capa --version
macOS
Apple Silicon (M1 and later)
$ curl -L -o capa \
https://github.com/nelsonduarte/capa-language/releases/latest/download/capa-macos-arm64
$ chmod +x capa
$ xattr -d com.apple.quarantine capa # bypass Gatekeeper
$ ./capa --run hello.capa
Without the xattr step, double-clicking
in Finder triggers Gatekeeper's "capa cannot be
opened because the developer cannot be verified"
dialog. The download is unsigned by design (no Apple
Developer ID); the quarantine attribute is set by
the browser, not the binary itself.
To run capa from any directory, move it
to ~/.local/bin and ensure that
directory is on your PATH (see the
PATH callout under Option A above):
$ mkdir -p ~/.local/bin
$ mv capa ~/.local/bin/
$ capa --version
Intel Macs are not shipped as a pre-built binary;
install from source (clone the repository, run
pip install -e . with Python 3.10+, then
use python -m capa).
Windows
x86_64
PS> Invoke-WebRequest `
-Uri "https://github.com/nelsonduarte/capa-language/releases/latest/download/capa-windows-x86_64.exe" `
-OutFile capa.exe
PS> .\capa.exe --run hello.capa
Each binary ships with a matching .sha256 file. Verify the download before running:
$ curl -LO https://github.com/nelsonduarte/capa-language/releases/latest/download/capa-linux-x86_64.sha256
$ sha256sum -c capa-linux-x86_64.sha256
capa-linux-x86_64: OK
PS> Invoke-WebRequest -Uri "...capa-windows-x86_64.exe.sha256" -OutFile sum.txt
PS> (Get-FileHash capa.exe -Algorithm SHA256).Hash -eq `
(Get-Content sum.txt).Split(" ")[0].ToUpper()
True
First-run latency. PyInstaller's single-file binary self-extracts to a temp directory on first run (~100-300 ms slower than python -m capa). Subsequent runs are cached and as fast as the source install.
macOS Gatekeeper. The binary is not (yet) Apple-notarised, so macOS will refuse to run it until you remove the quarantine attribute (xattr -d above) or whitelist it in System Settings → Privacy & Security. Future releases will move to a signed and notarised build.
Option C: from source
Requirements: Python 3.10 or newer, git. Tested on 3.10, 3.12, and 3.14 across Linux, macOS, and Windows. Zero runtime dependencies outside the standard library.
Clone and install in editable mode:
$ git clone https://github.com/nelsonduarte/capa-language
$ cd capa
$ pip install -e .
That's it. You can verify the install by running the test suite (3160 tests, a couple of minutes):
$ python -m unittest discover tests
Why pip install -e . matters. The editable install registers the capa package with Python's import system and adds a capa command on your PATH. After it, both capa <args> and python -m capa <args> work from any directory. Without it, python -m capa only resolves while your current directory is the repo root (Python walks the cwd-relative capa/ folder). Running python -m capa init my-project from your Desktop in that case fails with No module named capa. The fix is the install above, or the binary path.
Optional: the Wasm backend. pip install -e '.[wasm]' pulls in wasmtime>=20 and unlocks capa --wasm --run, capa --wasm --component --run, and capa --wasm --component --output app.wasm. Passing --prefer-wasm (or setting CAPA_PREFER_WASM=1) makes a plain capa --run try the Wasm backend first and fall back to the Python pipeline silently when a program uses something the Wasm emitter does not yet handle.
Packages
Capa projects declare their dependencies in a capa.toml file at the project root. The same file that says "this project depends on capa_log v0.1" pins the source: a git URL with an exact tag, or a relative path on disk. A signed registry index maps short package names to those git sources, so capa add capa_log resolves the name through the index and writes the dependency for you; the index is itself GPG-verified against a pinned key before any name is trusted. A direct git URL still works without the registry for anything the index does not list.
# capa.toml
[package]
name = "my-project"
version = "0.1.0"
capa = ">=0.8.4"
[dependencies]
capa_log = { git = "https://github.com/nelsonduarte/capa_log", tag = "v0.1.2" }
[dev-dependencies]
capa_test = { git = "https://github.com/nelsonduarte/capa_test", tag = "v0.1.1" }
A [dev-dependencies] table holds test- and tooling-only deps (the same schema as [dependencies]); they vendor in when you build the project itself but are never pulled in when the package is consumed as someone else's dependency. capa add <name> and capa add --dev <name> declare a dependency from the command line, resolving a short name through the signed registry index.
Three commands cover the day-to-day:
$ capa install # resolve capa.toml, fetch git deps into ./vendor/, write capa.lock
$ capa --run main.capa
$ capa install # idempotent: rerun to refresh against capa.lock
When a capa.toml is present, the loader picks up ./vendor/ and the parent of every path = entry automatically; no CAPA_PATH needed. The capa.lock file pins the resolved git SHA plus a SHA-256 of the fetched tarball so subsequent installs are bit-reproducible. When a dependency carries a verify_key (a GPG public key for tag-signature verification or a Sigstore identity for SLSA L2 attestations), capa install runs the corresponding verification before unpacking and refuses on mismatch.
Full guide at docs/packages.md: declaration syntax, lockfile format, path vs git deps, attenuated capability re-export across package boundaries, and the three-layer supply-chain stack (lockfile SHA + GPG tag signing + SLSA L2 via Sigstore Rekor).
Seed libraries
The signed registry index currently lists eight libraries. Each declares its own capability surface, so its authority is visible in the SBOM before you ever read its code. The capability column is the genuine honest claim: a library marked "none" is provably pure.
| Package | What it does | Capability |
|---|---|---|
capa_cli | Command-line argument parsing: flags, positionals, subcommands | none |
capa_csv | RFC 4180 CSV parser, header view, and writer | none |
capa_datetime | Date and time values plus formatting | none (no Clock needed to format) |
capa_hash | SHA-256, SHA-224, and HMAC-SHA256, with constant-time tag comparison | none |
capa_http | HTTP client: request and response handling | Net |
capa_log | Structured logging with levels and formatters | Stdio |
capa_sbom | SBOM parsing for CycloneDX and SPDX JSON with capa:* capability queries | none |
capa_test | A tiny assertion library for the capa test runner | Stdio |
Testing your project
The capa test subcommand discovers and runs the test programs under tests/test_*.capa in your project. A test passes when it exits 0 (a panic fails it); on failure the captured stdout is printed inline.
$ capa test # Python backend (same pipeline as capa --run)
$ capa test --wasm # Wasm backend
$ capa test --both # both backends, plus a cross-backend stdout diff
capa test --both runs every test on the Python and the Wasm backend and reports any output divergence as its own failure kind, which is the cheapest cross-backend parity check a library can run. Test-only dependencies belong in the [dev-dependencies] table; capa test refuses up front, with a pointer at capa install, if a dep is not vendored yet.
Your first program
The fastest path is to let the compiler scaffold one for you:
$ capa init my-project
$ cd my-project
$ capa --run main.capa
Hello from Capa!
capa init creates four files: main.capa (a runnable starter that already uses Stdio so the capability discipline is visible from the first line), README.md, .gitignore, and .capa-version (which pins the version used at scaffold time, useful for reproducibility). Pass . as the name to scaffold into the current directory (which must be empty), or omit the argument entirely.
If you prefer to type the file yourself, create hello.capa with the following contents:
// hello.capa
fun main(stdio: Stdio)
stdio.println("Hello, Capa!")
Run it:
$ capa --run hello.capa
Hello, Capa!
The stdio: Stdio parameter is the language asking explicitly for the right to write to standard output. The runtime hands one to main, and main can hand it further down, or not. This is the whole language in one line.
The CLI
The compiler exposes pipeline modes (each subsumes the previous: --run implies --transpile, which implies --check) plus a few tooling flags for formatting, documentation, and manifests.
| Flag | What it does |
|---|---|
init [name] |
Scaffold a new Capa project (subcommand, not a flag). Creates main.capa, README.md, .gitignore, .capa-version. |
(none) |
Tokenize. Print the stream of lexer tokens. Useful for debugging lexer rules. |
--parse |
Parse to AST and print a structured dump. Useful for debugging the grammar. |
--check |
Run the full analyzer (name resolution, types, capability discipline). Prints errors with source-aligned context, or OK on success. |
--transpile |
Emit equivalent Python 3.10+ source to stdout. Useful for understanding how Capa lowers a construct. |
--run |
Transpile and execute. This is the everyday flag. |
--fmt / --fmt-check |
Rewrite the file in canonical Capa style (whitespace, indentation, blank-line clusters, final newline), or verify only. |
--doc |
Emit a self-contained HTML page documenting the file, built from /// and /** */ doc comments. |
--manifest / --cyclonedx / --spdx / --vex / --provenance |
Emit the authority graph in five standard machine-readable formats: Capa-native JSON (--manifest), CycloneDX 1.5 SBOM, SPDX 2.3 SBOM, CycloneDX VEX (per-function exploitability claims), and SLSA Build L1 provenance attestation. All derived from capability signatures at compile time. |
--watch |
Re-run the program every time it (or any of its imported modules) changes on disk. Implies --run. Ctrl-C to exit. Pairs well with the language tour for iterative experimentation. |
lsp |
Start the language server on stdio (subcommand). Works out of the box from the standalone binary (the language server is bundled); a pip install needs the [lsp] extra. See the next section for editor configuration. |
Common invocations:
$ capa --check examples/io.capa
OK
$ capa --run examples/grades.capa
=== Roster ===
Ana: 17.5 (Excellent)
Bruno: 13.0 (Pass)
...
$ capa --transpile examples/hello.capa | head -20
Every invocation above also works as python -m capa <args> if the capa command is not on your PATH (e.g. you cloned the repo but did not pip install -e .).
Two adjacent topics are reference material rather than setup: visibility (pub) and module search paths (CAPA_PATH) live in the language reference.
Editor integration
Capa ships a language server, so any LSP-capable editor (Helix, Neovim, Zed, VSCode with an LSP client, JetBrains, Emacs, ...) gets diagnostics, hover, go-to-definition, find-references, the outline view, and Quick Fixes for "did you mean?" hints without round-tripping to the terminal.
If you installed the standalone binary (Option A or B above), the language server is already bundled, so just launch it:
$ capa lsp # speaks LSP over stdio
If you installed from source with pip instead, add the optional [lsp] extra first:
$ pip install -e '.[lsp]' # adds pygls>=2.0
$ capa lsp # speaks LSP over stdio
The server delivers twelve features (v1 surface plus the v2 polish round). None are configurable; the server runs the same lexer / parser / analyzer the CLI runs.
| LSP feature | Behaviour in Capa |
|---|---|
publishDiagnostics |
Full pipeline on every didOpen / didChange / didSave. Errors carry the ; did you mean 'X'? hints when applicable. |
hover |
Cursor over a function shows the Capa signature; over a parameter, binding, constant, field, variant, or capability shows name: T plus a kind label. Fires on references and declaration sites. |
definition |
Jump from any reference (or the declaration itself) to the precise column of the declared name. Built-ins are filtered cleanly. |
references |
List every identifier in the file resolving to the same symbol; includeDeclaration is honoured. |
documentSymbol |
Hierarchical outline. Constants, structs (with fields), sum types (with variants), traits / capabilities (with method signatures), functions, and impl blocks (with methods) in source order. |
codeAction |
One-click Quick Fix for every ; did you mean 'X'? diagnostic; marked preferred. |
rename / prepareRename |
Renames the symbol under the cursor by rewriting every reference and the declaration in one workspace edit. New name validated as a Capa identifier (rejects reserved keywords); built-ins refuse rename cleanly. |
completion |
Suggests keywords, built-in types / capabilities / variants / functions, module-level names with their signatures or declared types, and the function-scope locals visible at the cursor. After a ., the list narrows to the methods of the receiver's type (with full signatures) for both built-in types and user-defined ones. Half-typed buffers that fail to parse fall back to the keyword + built-in floor for general context, and to a placeholder-retry parse for method context, so the suggestion list never goes dark. |
semanticTokens/full |
Type-aware highlighting. Functions, parameters, variables (with readonly for let and constants), capabilities (defaultLibrary on the built-ins), types, sum variants, and struct fields are coloured distinctly. Editors that support semantic tokens layer this on top of the TextMate grammar. |
documentHighlight |
Cursor on an identifier highlights every in-file occurrence resolving to the same symbol. v1 emits DocumentHighlightKind.Text uniformly; read / write distinction deferred. |
foldingRange |
Gutter +/- regions for function, struct, sum, impl, trait, and control-flow bodies (if, for, while, match, lambda). Returns empty on parse failure so a mid-edit file does not confuse the editor. |
formatting / rangeFormatting |
Format Document and Format Selection, backed by capa.formatter.format_source (the v3 AST round-trip pipeline). rangeFormatting falls back to whole-document since v3 is parse-then-emit; the formatter never raises, so the handlers never raise either. |
Client configuration
One-liner for Helix (languages.toml):
[[language]]
name = "capa"
language-servers = ["capa"]
file-types = ["capa"]
[language-server.capa]
command = "capa"
args = ["lsp"]
For Neovim with nvim-lspconfig:
require("lspconfig").configs.capa = {
default_config = {
cmd = { "capa", "lsp" },
filetypes = { "capa" },
root_dir = require("lspconfig.util").root_pattern(".git", "."),
},
}
require("lspconfig").capa.setup({})
If capa is not on your PATH, swap command = "capa" / cmd = { "capa", ... } for the explicit command = "python", args = ["-m", "capa", "lsp"] form (and the matching Neovim version).
Remaining v2 polish (deferred until a real-user session surfaces a specific need): signatureHelp, inlayHint, workspace/symbol, codeLens, selectionRange.
Programmatic use
The compiler is also a library. The four-stage pipeline is exposed directly so you can embed any stage into a host program.
from capa import Lexer, Parser, analyze, transpile
source = open("program.capa", encoding="utf-8").read()
tokens = Lexer(source, filename="program.capa").lex()
module = Parser(tokens, source=source, filename="program.capa").parse_module()
result = analyze(module, source=source, filename="program.capa")
if not result.ok:
for e in result.errors:
print(e.format())
else:
code = transpile(module, filename="program.capa")
print(code)
Editor support
The Capa VSCode extension is published on the Visual Studio Marketplace. Search for "Capa" in the Extensions view, or install it from the command line:
$ code --install-extension nelsonduarte.capa-language
It provides syntax highlighting (TextMate grammar), keywords by category, distinct colour for built-in capabilities, string interpolation, and operator highlighting including .., ..=, =>, and ?. It also bundles an LSP client, so it auto-connects to capa lsp on your PATH and brings the full language server (diagnostics, hover, go-to-definition, references, symbols, code actions, rename, completion, semantic tokens) into VSCode with no manual configuration. Code snippets are included too.
To work from the repository source instead (for example to hack on the grammar), the extension also lives in the vscode/ directory; symlink or junction it into your extensions folder:
$ ln -s "$(pwd)/vscode" ~/.vscode/extensions/capa-language
PS> New-Item -ItemType Junction `
-Path "$HOME\.vscode\extensions\capa-language" `
-Target "$PWD\vscode"
Reload VSCode and any .capa file will be highlighted. The bundled LSP client connects to capa lsp automatically; the full language server feature set is described under Editor integration above.
Where to go next
Read the case →
Why Capa exists, what problem it addresses, and what it does not claim to solve.
Migrate from Python →
Already have a Python program? Move it into Capa one function at a time, shrinking the Unsafe surface as the authority manifest gets more honest.
Tour the language →
A guided pass through the syntax: types, control flow, generics, capabilities, attenuation.
Read the examples →
Over fifty runnable Capa programs in the repository, from hello.capa to user-defined capabilities, SBOM parsers, the event-stream demo, and the CVE case studies.
Read the positioning →
What is and is not unique about Capa, honestly: where the type system stands against adjacent work, and the one-sentence claim Capa defends.