Skip to content

perch

🚫 perch is NOT accepting external contributions at this time.
Pull requests will be closed unread. Feature ideas β†’ GitHub Discussions. Bug reports for shipped behavior β†’ issues (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.

One file. Every surface. Define your commands once in a .perch file β€” run them as a CLI, a web UI πŸͺŸ, a REPL, an AI-agent tool πŸ€–, or a portable binary. macOS Β· Linux Β· Windows.

πŸ“
One file
Replaces bash + Make + a Cobra/Click/Typer CLI
πŸͺŸ
Web UI
No frontend to write β€” --server is the dashboard
πŸ€–
AI agent tool
Same file as MCP server β€” typed verbs, capability gates
πŸ“¦
One binary
--build ships a portable executable, no Go needed
πŸ›‘οΈ
Safe by default
SSRF/redirect guards Β· capability flags Β· audit log
⚑
Cross-platform
macOS Β· Linux Β· Windows Β· same 146 built-in ops

Same redis.perch file, five rendering modes β€” animation cycles through each.

0
built-in ops
0
auto-bound vars
0
frontends
0
restriction flags
0
binary to ship

The three things perch does that nothing else does

🎯 One file β†’ five surfaces

One commands.perch β†’ CLI Β· web UI Β· REPL Β· MCP tool Β· portable binary. Not five integrations β€” one abstraction, five renderings.

Nothing in bash / Make / Click / Cobra / Just / Task does this.

πŸ”’ wasm_run β€” sandbox by construction

Load a WebAssembly module. It sees ONLY the argv, env vars, and mounts you declared β€” anything else doesn't exist. Not policy. Construction.

🎯 Killer demo: zero-trust AI plugin runtime · reference · 5 walkthroughs

πŸ§ͺ perch simulate β€” "what would happen on THAT host?" v2

Walk the program against a hypothetical env. Per-op verdicts: WILL_RUN βœ“ Β· WILL_FAIL βœ— Β· MIGHT_FAIL ?. v2: state threading, oracles, multi-scenario.

Details β†’

Also recently shipped β€” everything else that landed

  • πŸ“œ requires manifest β€” declare every external resource (bin Β· env Β· host Β· read/write Β· OS Β· arch). Every external op verifies it before running; undeclared access errors. Gated, every time β†’
  • πŸ” SHA-256 bin pinning β€” hash "sha256:…" / hash_file "bundle:…" compares bytes on disk; never executes the binary.
  • πŸ–₯️ OS + arch context blocks β€” os "…" … end Β· arch "…" … end declare cross-platform/arch branches structurally; compose for matrix builds. simulate prunes mismatched leaves as dead code.
  • πŸͺ’ Keyword-free dispatchbreaking β€” no run/call: a bare name invokes a command, expands a template, or runs a declared bin (deploy -target=linux). Names are globally unique, so it's unambiguous; --check errors on any collision.
  • 🏷️ Bare top-level bindings + bin … asbreaking β€” declare shared values bare (no globals block); reference them in requires by name (write BUILD_DIR); give a path-bin a handle (bin "./bins/x" as x).
  • πŸ”’ Catch needs proxy_argsbreaking β€” the catchβ†’shell forwarding --scan flagged HIGH can't happen implicitly; ${proxy_args} is unbound without the modifier.
  • πŸ”’ Version checks like math β€” assert_version "${v}" >= "1.28.0" (infix) + version_extract to pull a version from any tool's output. Semver-aware.
  • 🎯 Risk score in --scan β€” one glance: 🟒 SAFE / 🟑 LOW / 🟠 MED / πŸ”΄ HIGH with concrete reasons; surfaced as a colored pill in the web UI.
  • πŸ›Ÿ try/rescue/finally + match β€” 30-kind error enum (shell_exit_nonzero, http_ssrf_blocked, …) for structured recovery + cleanup.
  • πŸͺŸ Web UI for non-devs β€” the same .perch file served as a tabbed localhost app (Run / Simulate / Scan / Check / About).
  • πŸ“‘ MCP live streaming β€” per-line stdout/stderr as notifications/progress events; no silent waits on long verbs.
  • πŸ“¦ 22 ready-made recipes β€” Redis Β· Postgres Β· devstack Β· aistack Β· kafka Β· modern-unix Β· gh-flow Β· docker-mgr Β· mkcert Β· backup Β· scan-secrets.
  • πŸ§ͺ 3 runnable wasm_run demos β€” schema validator, K8s policy check, agent-safe diff summarizer.
  • πŸ“¦ Declarative bundle + aliases β€” include "./mod.wasm" as mod β†’ wasm_run mod (bare ident, zero disk reads).

The philosophy β€” what perch is trying to do

Every team has an operational layer: the scripts that build the project, spin up the dev stack, deploy the service, wrap the clunky CLI, run the runbook. It's the least-loved code you own β€” and you usually write it five times in five incompatible languages. A bash script for the terminal. A Makefile for CI. A Cobra/Click/Typer program when the bash gets too hairy. A little Flask dashboard so non-devs can click a button. An MCP/JSON-tool backend now that an agent needs to run it too. Five descriptions of the same handful of verbs, drifting apart the moment they're written.

perch's bet is that this is one program, not five β€” and that you should describe it once.

β‘  Describe once, render many

You declare typed verbs in one .perch file. Being a CLI, a web form, a REPL command, an MCP tool, and a binary entry point is the tool's job β€” not yours. No schema written five times, nothing to keep in sync.

β‘‘ The manifest is the contract

Capabilities are declared and gated, not bolted on after. A file states what it touches (requires); the operator narrows it (--no-*); neither side can exceed the other. You can know what a file intends before you run it. Honest scope: perch enforces its own ops and scrubs the subprocess env to the manifest β€” a declared bin's filesystem/network still needs an OS sandbox underneath. What we can and can't enforce β†’

β‘’ Cross-platform is the runtime's problem

Common operations β€” copy, mkdir, hash, http, gzip β€” are first-class ops, not per-OS shell incantations. exec runs declared binaries without a shell. The same file behaves the same on macOS, Linux, and Windows.

β‘£ Legible by construction

Typed args, --check, --scan, simulate, a structured error enum, and an audit log come standard. Operational code is the code most often run by someone who didn't write it β€” so it has to be readable and inspectable, not clever.

β‘€ Ship one artifact

Hand someone a file, or --build a single portable binary that needs no Go, no perch, no clone. The install tax on the recipient should be one download β€” and embedding can carry an entire Python/Node project along.

β‘₯ Stay small on purpose

perch is a control plane, not a programming language. It orchestrates tools and glues steps together; it is deliberately not where you write your application's business logic. A small, closed vocabulary is what makes the other five promises keepable.

The throughline: the operational layer should be something you write once, read easily, hand to a teammate β€” or an AI agent, or CI β€” with confidence, and run anywhere. Everything else perch ships is in service of that one goal.

Want the longer argument? sandboxed-by-design and trust-by-manifest lay out the security worldview; os-in-a-program covers the cross-platform stance.

What perch enforces today β€” and what it doesn't (be skeptical of the word 'sandbox')

perch is controlled scripting, not a kernel sandbox. Here is the honest line between what the manifest actually enforces and what it can't:

Concern Status today
perch's own ops (http_get, read_file, write_file, exec bin check) βœ… Enforced β€” undeclared access errors, every time
Which binaries run βœ… Enforced β€” only declared bins spawn (bin_not_declared)
Subprocess environment βœ… Scrubbed β€” a spawned tool sees only declared env + a default operational set (PATH/HOME/…); undeclared secrets are dropped
A subprocess's own filesystem & network ⚠️ Not enforced β€” once git/docker runs, perch can't parse its args; requires read/write/host bound perch's ops, not the tool's. Needs OS confinement (sandbox-exec, Landlock, firejail) β€” on the roadmap
Genuinely adversarial .perch files ⚠️ Run perch itself inside a container/VM; the manifest describes intent, the OS enforces isolation

With --no-shell --no-subprocess the boundary is airtight (perch spawns nothing). With subprocesses allowed, treat the manifest as an honest declaration of intent + an env scrub, not a jail. Full discussion: sandboxed-by-design Β§ the subprocess trust boundary.


πŸͺŸ The web UI β€” two products in one binary

perch --server is a single feature with two distinct value props depending on who you are:

πŸ€– "I'm using AI agents and want to see what they're doing"

As more teams give AI agents access to their infra ("deploy my app", "restart that pod"), non-devs are increasingly downstream of automated work they can't see. perch --server is the "shows your work" companion to perch-mcp: same .perch file, agent uses the MCP surface, you watch every op stream live in the Run tab. Open πŸ§ͺ Simulate to ask "what would happen if the agent retried?" or πŸ” Scan to see what a verb actually does before granting it.

πŸŽ›οΈ "I just want to control my system from a UI"

You operate stuff β€” Docker, K8s, your home server, a small fleet β€” and you'd rather click than type the same kubectl … | jq … | grep … for the 400th time. You don't want to write a frontend, stand up Retool, or maintain Backstage. Declare your verbs in commands.perch; run perch --server; that's the UI. Add a command restart_pod, refresh the page, the form is there. No app to deploy. No CSS to write. The file is the dashboard.

Same engine for both audiences. Same .perch file, same interpreter, same capability gates. Add a verb β†’ it's a CLI command, a web UI form, an MCP tool, a REPL command, and a binary entry point. Four consumers, zero duplicate schemas.

perch -f commands.perch --server --port 8080
# β†’ open http://127.0.0.1:8080

Here's what they see

http://127.0.0.1:8080

πŸͺΆ deploy

Production deploy + rollback workflows Β· v1.2.0 Β· deploy.perch Β· 6 commands πŸŒ“
β–Ά Run πŸ§ͺ Simulate πŸ” Scan βœ“ Check β„Ή About

deploy_canary proxy_args

Deploy the current build to one canary region; pin a baking window before promoting.
string int bool
Run Copy as CLI
[started] β–Έ if has_bin "kubectl" β†’ true βœ“ exec kubectl apply -f canary.yaml -n prod βœ“ exec kubectl rollout status deploy/api-canary β–Έ retry max=3 βœ“ http_get "https://api.example.com/health" β†’ 200 β–Έ wait 15m [ok] deploy_canary completed in 16m 02s

rollback_release test-allowed

Revert to the previous tagged release across all regions.
string
Run Copy as CLI

β–Ά Run tab β€” type-aware form per command, live NDJSON output, Copy-as-CLI hand-off

http://127.0.0.1:8080#simulate

πŸͺΆ deploy

Production deploy + rollback workflows Β· v1.2.0 πŸŒ“
β–Ά Run πŸ§ͺ Simulate πŸ” Scan βœ“ Check β„Ή About
select CSV CSV textarea
Simulate2 scenarios Β· 1 will-fail
═══ Scenario: happy-path ═══ βœ“ if has_bin "kubectl" oracle: true β†’ body runs βœ“ exec kubectl apply -f canary.yaml -n prod βœ“ http_get "https://api.example.com/health" oracle: 200 OK summary: 4 will-run Β· 0 will-fail Β· 0 uncertain ═══ Scenario: kubectl-missing ═══ βœ— if has_bin "kubectl" oracle: false β†’ body SKIPPED βœ— fail "deploy requires kubectl" 1 op(s) would fail across simulated scenarios β€” CI blocks the PR

πŸ§ͺ Simulate tab β€” "what would happen on the prod host if I ran this?" answered without a terminal

Five tabs, one file, zero config:

β–Ά Run

Searchable list of every command. Type-aware form inputs β€” checkbox for bools, number spinner for ints, multi-line textarea for rest args. Click Run β†’ output streams live. Copy as CLI button hands the form back as a shell command. Globals panel + mod badges (test Β· detached Β· proxy_args).

πŸ§ͺ Simulate

Every --sim-* flag becomes a form field. Paste a v2 fixture JSON (capabilities + oracles + scenarios) and Simulate β†’ per-op outcomes for each scenario side by side. *"What would this do on the prod host?"* answered without a terminal.

πŸ” Scan

One click β†’ the full capability + risk audit. Leads with a one-glance risk score (🟒 SAFE / 🟑 LOW / 🟠 MED / πŸ”΄ HIGH) and concrete reasons (uses sudo, executes shell, catch forwards proxy_args, …). Severity pills, the recommended hardened invocation, every host / write root / env var the program touches. Run this before executing anything you didn't write yourself.

βœ“ Check

One click β†’ syntactic validation (same engine as perch --check in pre-commit). Issue list with severity counts.

β„Ή About + Theme

Program metadata + doc links. Dark mode auto-respects prefers-color-scheme and persists per browser. Hash-routed tabs (#run, #simulate, …) so any tab is bookmarkable.

πŸ”Œ JSON API

Every panel is a JSON endpoint you can drive from another internal tool: GET /api/program, POST /api/check / /api/scan / /api/simulate, NDJSON-streaming /api/exec. Embed perch in your dashboard, Slack bot, or Backstage plugin.

Security: single-tenant + localhost-bound by default; pair with your reverse proxy + SSO for shared access. Capability restrictions inherit from launch β€” perch --no-shell --no-network --server produces a UI where shell ops error and HTTP is denied. Commands marked private are hidden + rejected.

Web UI guide β†’

See them in action


What perch replaces

Today Tomorrow with perch
πŸ“œ bin/ of bash scripts + a Makefile + a CI YAML duplicating both πŸͺΆ One commands.perch file that local dev, CI, and on-call all execute
πŸ› οΈ A bespoke Cobra / Click / Typer CLI with hand-rolled arg parsing πŸͺΆ Typed args in declared verbs with per-command --help for free
πŸ€– A FastAPI service exposing safe ops to an LLM agent πŸͺΆ perch-mcp reads the same file β€” typed tools, capability gates, audit
πŸ“‹ A wiki page telling new hires which scripts to run πŸͺΆ perch --help + an optional perch --server web UI
πŸ“¦ "First install Python 3.11, then a venv, then pip install …" πŸͺΆ perch --build ships one binary with the project embedded
πŸͺ΅ ad-hoc echo-style logging + manual screenshot of CI output πŸͺΆ --audit FILE.ndjson structured trace + --report span tree
πŸ§ͺ "Run it and see" as the only test strategy πŸͺΆ perch test sandboxed behavior tests with assert_* ops

Adoption is incremental. Wrap your existing .sh files in a shell op; gain typed args + --help + audit + MCP in minutes. Promote to native ops over time. Migration guide β†’


πŸ“¦ Ready-made recipes β€” install in one curl

22 curated .perch files that solve real problems. Local Redis, the whole AI/observability/Kafka stack, cross-platform tool installers, daily Docker/kubectl wrappers. Download one, audit with perch --scan, run it.

# Pick one. Run it.
curl -fsSL https://raw.githubusercontent.com/olivierdevelops/perch/main/recipes/redis.perch -o redis.perch
curl -fsSL https://raw.githubusercontent.com/olivierdevelops/perch/main/recipes/_lib.perch -o _lib.perch
perch --scan -f redis.perch        # audit before running
perch -f redis.perch up             # 8 verbs ready: up / down / cli / flush / monitor / logs / backup / status
Pain Recipe One command
"I need Postgres + Redis + S3 locally for my web app" devstack perch -f devstack.perch up (Postgres + Redis + MinIO in parallel)
"I want to play with local LLMs" aistack perch -f aistack.perch up (Ollama + ChromaDB + Open WebUI)
"I need local metrics + logs + dashboards" observe perch -f observe.perch up (Prometheus + Grafana + Loki)
"I keep typing 12 docker flags wrong" docker-mgr perch -f docker-mgr.perch prune_safe
"Three teammates wrote three different git pr aliases" gh-flow perch -f gh-flow.perch pr / land / sync / cleanup
"I want every modern CLI tool installed cross-platform" modern-unix perch -f modern-unix.perch install (ripgrep, fd, bat, fzf, jq, yq, eza, zoxide)
"Set up local HTTPS for dev" mkcert-local perch -f mkcert-local.perch install_ca && perch -f mkcert-local.perch cert localhost dev.local
"Encrypted backups, no SaaS" backup perch -f backup.perch snapshot ~/Documents (restic wrapper)

Browse all 22 recipes β†’

The full catalog: single services (redis, postgres, mongodb, mysql, mailpit, minio, rabbitmq, localstack), stacks (devstack, aistack, observe, kafka-stack), tool installers (modern-unix, clouds, node-stack, python-stack), CLI wrappers (gh-flow, docker-mgr, kube-helpers), ops/security (mkcert-local, backup, scan-secrets).


Why teams adopt perch

πŸ›‘οΈ Safe by composition

Restriction flags compose β€” --no-shell --no-network --env HOME,PATH. Default-on SSRF and redirect guards. --scan audits a file before you run it. perch gates its own ops and scrubs the subprocess env β€” controlled scripting, not a kernel sandbox (honest scope).

πŸ€– Agent-native, no backend

Replace your LLM-tool backend with a .perch file. perch-mcp --no-shell --no-network -f ops.perch is the whole stack. Typed verbs, declared schemas, capability gates, audit log. Live streaming over MCP progress notifications β€” long-running verbs emit per-line stdout/stderr events as they run instead of returning a silent blob. Streaming β†’

πŸš€ Zero-install recipients

perch --build -o myapp produces a single executable. Recipients run one file β€” no Go, no perch, no source clone. --include ./src embeds an entire Python / Node project alongside.

πŸ§ͺ Behavior tests built in

Mark a command test. perch test runs it in a sandboxed temp cwd with --no-shell / --no-network / --no-subprocess on by default. Seven assert_* ops. Drop into pre-commit + CI. Details β†’

πŸ“Š Full visibility β€” --trace Β· --audit Β· --report

--trace streams every op to stderr as it fires (live, indented for block nesting). --audit FILE.ndjson writes the same events as JSON for downstream ingest. --report renders the span tree after the run with full error context. Same hook order, three audiences.


Who this is for

Platform & DevEx teams

You maintain the bin/ folder of bash scripts, the Makefile no one trusts on Windows, and the README that tells new hires which steps to skip. Replace all three with one shippable binary.

SREs & on-call

Wrap kubectl / docker / rsync / openssl behind named verbs with typed args. Give the support team a web UI for safe runbooks. Stream every action as NDJSON straight to your audit pipeline.

Tool authors shipping internal CLIs

Embed a Python or Node project inside a single binary that installs itself into a hash-addressed cache, sets up its own venv, and drops a launcher in $PATH. Recipients need no Go, no pip, no clone.

Teams building with AI agents

Replace your LLM-tool backend with a .perch file. Typed args, declared verbs, composable restrictions β€” perch-mcp --no-shell --no-network --env KUBECONFIG -f ops.perch is the whole backend. See how β†’


Sound familiar?

- 🧱 Your Makefile silently breaks on Windows / on Apple Silicon / on the new intern's machine. - πŸ“œ Your `bin/` folder has 40 bash scripts and exactly one person who knows which to run. - πŸ§ͺ You ship a tool internally and spend a week answering install questions instead of building. - πŸ€– An LLM agent needs to drive your infra and you're not handing it a shell. Ever. - 🧰 Your "internal CLI" is six Python packages, a Node helper, and a stern Slack message. - πŸͺŸ "Windows is a stretch goal" has been on the roadmap for three quarters.

perch is one DSL, one runtime, and one binary that solves all of these together.


Three commands to start

# 1) install
go install github.com/olivierdevelops/perch@latest

# 2) scaffold
perch --init

# 3) explore
perch --help          # list commands
perch <cmd> --help    # per-command help (args, defaults, examples)
perch --check         # static validation
perch test            # run every command marked `test` (sandboxed)
perch simulate cmd --sim-os=linux --sim-have-bin=kubectl  # what would happen on THAT host?
perch simulate cmd --sim-file fixture.json   # multi-scenario: happy / github-down / kubectl-missing
perch --report cmd    # execute + render the span tree

Cross-platform without thinking about it

~30 variables auto-bound at every command start. No declaration, no let, no if uname. Hover any row below β€” that's what perch sees on the running machine.

What's in the box

~140 cross-platform ops

cp, mkdir, gzip, tar_create, http_get, download, sha256_file, regex_replace, json_get, bundle_extract, … all implemented in Go, identical on macOS / Linux / Windows. Skip the bash tax. Catalog β†’

Static --check validator

Catches typo'd arg types, mismatched defaults, duplicate args, colliding positional indexes, missing run TARGET, unknown ops, unresolved ${name} placeholders β€” before any command runs. Wire it into pre-commit.

Templates & execution contexts

Wrap any body in parallel, timeout, retry, with_env, with_cwd, sandbox, or cache. Lift repetition into template NAME ... end parameter-substitution stamps. Details β†’

Unified if EXPR ... end

Comparisons (if os == "linux"), truthy/falsy (if has_bin, if not has_bin), predicate calls (if exists "./bin"), numeric (if size > 1000000). One block, every shape.

Preview before running

--dry-run prints every op with interpolated args and skips execution. --ask prompts y/n/a/q per op. --scan walks the program statically and reports needed capabilities + risk findings. No surprises in CI.

Editor integration

perch-lsp provides diagnostics, completion, hover, document outline. perch --install-vscode bundles the VS Code extension; --install-lsp alone for Neovim / Helix / Zed. Tree-sitter grammar for syntax beyond LSP. Setup β†’

Catch passthrough & fuzzy suggestions

Levenshtein-based "Did you mean…?" for typo'd command names. Inside catch, ${proxy_args} holds the full unknown invocation β€” shell "git ${proxy_args}" makes perch a drop-in superset of any tool.

Block-shaped args + per-command --help

arg NAME ... end with labelled inner fields (type, default, description, optional, index, rest). perch <cmd> --help renders usage + table from the spec. No manual doc-strings.


Four use cases enterprise teams recognise

1. Replace the Makefile that everyone is afraid of

Before: a 400-line Makefile with shell variations behind every target; Windows users on WSL; the CI YAML reimplements half of it; nobody touches test-integration because the last person who did is now in a different company.

After: one commands.perch shared by local dev and CI. perch --check runs in pre-commit. perch --help is the README.

command test
    description "Run unit + integration tests"
    do
        go test -race ./...
        if exists "./integration"
            go test -tags=integration ./integration/...
        end
    end
end

β†’ Walkthrough: tutorials/01-replace-your-makefile.md

2. Ship a Python / Node / monorepo project as one self-installing binary

Before: "first install pyenv, then python 3.11, then a venv, then pip install -r requirements.txt, then…" Three pages of README and a Slack channel for install help.

After: you hand them stt_bin. They run ./stt_bin install. The binary extracts an embedded archive into ~/.cache/perch/<hash>/, creates a venv, runs pip install, drops a launcher in ~/.local/bin/stt. Done.

perch --build -f commands.perch --include ./src -o stt_bin
scp stt_bin user@server:/usr/local/bin/
ssh user@server 'stt_bin install && stt example.wav'

The recipient needs only what your install command requires (here: python3). No package manager. No registry. No internet at install time.

β†’ Worked example: demos/05-python-installer

3. Give AI agents a safe operations surface β€” without standing up a backend

Deep dive: LLM control plane β€” why a .perch file + perch-mcp + a few CLI flags replaces 2,000 lines of FastAPI scaffolding.

Before: the agent gets a shell. You hope. You write a long system prompt about what it should and shouldn't do. You audit logs after the fact.

After: the agent gets perch-mcp pointed at ops.perch. It can call exactly the verbs you declared, with exactly the arg types you declared. Anything else returns a typed error. No shell escape ever.

requires
    bin "ssh"          # the file declares the one tool it shells out to
end

command restart_service
    description "Restart a service on a host"
    arg host
        type string
        description "Hostname (must match /^[a-z0-9.-]+$/)"
    end
    arg service
        type string
        description "Service name (one of: web, worker, scheduler)"
    end
    do
        if not regex_match "${host}" "^[a-z0-9.-]+$"
            fail "invalid hostname"
        end
        ssh "${host}" systemctl restart "${service}"
    end
end

The agent never sees ssh. It sees restart_service(host, service) with typed args. The schema is the security boundary.

β†’ Details: mcp.md

4. Wrap a clunky CLI behind sane verbs

Before: every team member memorises 12 docker flags. Mistakes cost an afternoon. The Slack channel has the same three questions every week.

After: ship dev (a perch binary). The team types dev up, dev logs, dev shell, dev reset. Unknown verbs fall through to docker via catch passthrough, so power users lose nothing.

requires
    bin "docker"       # everything this file runs is declared
end

command up
    description "Start the dev stack"
    do
        docker compose up -d                 # shell-free: structured argv, no metachar surface
        docker compose exec api migrate up
        print "βœ“ Stack running at http://localhost:8080"
    end
end

catch passthrough
    description "Forward unknown commands to docker"
    proxy_args                       # explicit opt-in to bind ${proxy_args}
    do
        shell "docker ${proxy_args}"             # proxy_args must word-split β†’ shell (the escape case)
    end
end

One binary. Onboarding goes from "read this 6-page doc" to "run dev up."

β†’ More patterns: applications.md


For platform / SRE / security teams

The questions enterprise teams ask up-front, answered in one place:

πŸ›‘οΈ Security model

Capability gating, not kernel sandboxing. Composable --no-shell / --no-network / --no-write / --no-subprocess flags. --env A,B,C restricts host-env visibility. --allow-bin git,docker narrows shell to argv[0]. --allow-host api.github.com restricts network. Layer with firejail / sandbox-exec / AppContainer for genuinely adversarial input. sandbox.md β†’

🧾 Audit + replay

--audit FILE.ndjson records every op call with timestamp, args, duration, error, exit code, and bound-variable state. Same shape as Linux auditd but at the op level. Pipe to Loki / Datadog / CloudWatch. --report renders the same stream as a human-readable span tree after the run.

πŸ€– AI agent safety

The MCP boundary is the file's grammar. Agents call declared verbs with typed args β€” anything else is a typed error. perch-mcp --no-shell --no-network --env KUBECONFIG -f ops.perch is the full policy. No FastAPI scaffolding, no manual JSON-Schema, no agent-readable shell escape. LLM control plane β†’

πŸ”’ SSRF + redirect protection (default-on)

HTTP ops refuse loopback / link-local / RFC 1918 / IPv6 ULA destinations by default — closes the AWS metadata SSRF (169.254.169.254). https→http downgrades refused. Max 5 redirect hops, each re-validated (DNS-rebinding defense via multi-A check). Layer --allow-host for a strict allowlist.

πŸͺŸ Cross-platform parity

The ~140 ops are identical Go implementations across macOS / Linux / Windows. With --no-shell the boundary is airtight (no subprocess can fire). Real "works on my machine" elimination, not aspiration.

πŸ“œ License + dependencies

Apache-2.0. One Go binary, no SaaS, no telemetry, no phone-home. Self-host or `go install`. Bundle into your own distribution. No license fees, no per-seat costs, no cloud account required. Source: github.com/olivierdevelops/perch.

πŸ§ͺ Pre-commit + CI integration

perch --check for static validation, perch test for behavior. Both exit non-zero on failure β€” wire into any CI. Per-test sandboxes prevent state leakage. The same .perch drives local dev, CI, and production. testing.md β†’

πŸ” Static audit of unknown scripts

perch --scan FILE walks a program WITHOUT executing it and reports: capabilities needed, env vars referenced, risk findings (sudo, catch passthrough to shell, unvalidated ${var} in shell args), and the tightest CLI invocation that should still let it run. Review third-party .perch files before adopting them.

πŸ“œ Declared requirements + supply-chain pinning

A requires block makes the file declare every external resource it touches β€” bins (with SHA-256 hash pins), env vars, hosts, filesystem read/write scopes, OS, arch. When present, every external op verifies the manifest immediately before executing, on every call (stateless β€” no allow-cache): undeclared shell bins, hosts, env reads, or filesystem paths all error. A regression test fails if any external op stops refusing undeclared access. perch --check additionally flags literal undeclared use at lint time β€” proving a file is feasible on a target host without running it. Hash pins (inline or hash_file "bundle:..." embedded in the fat binary) defend against PATH-shadow + trojaned mirrors, read-only. capability-gating.md β†’ Β· requires.md β†’. Where this is heading: zero ambient authority β€” programs start with NO external access at all; today the manifest is opt-in (a file without it keeps ambient access).

πŸ“Š Status & maturity

Pre-1.0 (v0.x). DSL surface is stable; op catalog continues to grow. SemVer applies once v1.0 is tagged. CI runs the full test suite on every commit. The repo eats its own dog food β€” commands.perch is what builds / tests / cleans perch itself.


How it compares

perch Make Just Task bash scripts Cobra/Click
Cross-platform without if uname βœ… ⚠️ ⚠️ βœ… ❌ βœ…
Typed args + per-command --help βœ… ❌ ❌ ⚠️ ❌ βœ…
Static validator (--check) βœ… ❌ ❌ ❌ ❌ ⚠️
Sandboxed behavior tests (perch test) βœ… ❌ ❌ ❌ ❌ ❌
parallel / retry / timeout / cache blocks βœ… ⚠️ ❌ ⚠️ ❌ ❌
Span-tree execution report (--report) βœ… ❌ ❌ ❌ ❌ ❌
Built-in web UI βœ… ❌ ❌ ❌ ❌ ❌
MCP server for AI agents βœ… ❌ ❌ ❌ ❌ ❌
Single-binary distribution βœ… ❌ ❌ ❌ ❌ βœ…
Embed source/data inside binary βœ… ❌ ❌ ❌ ❌ ⚠️
LSP + VS Code extension βœ… ❌ ⚠️ ⚠️ ❌ n/a
~70 portable ops (no bash) βœ… ❌ ❌ ⚠️ ❌ n/a
Zero-build authoring (no Go required) βœ… βœ… βœ… βœ… βœ… ❌

The 30-second tour


Common questions

How do I audit a .perch file I didn't write? perch --scan FILE walks the program statically β€” no execution β€” and reports:

  • Capabilities needed. Does it need shell? Which binaries? Network? Which hosts? Writes? Which paths?
  • Env vars referenced. Every ${UPPERCASE_NAME} it touches.
  • Risk findings. Sudo use, catch-passthrough to shell, unvalidated ${var} in shell args, downloads followed by make_executable, etc. Each rated HIGH / MED / LOW with a concrete fix suggestion.
  • Recommended invocation. The tightest CLI flag combination that should still let the script run β€” assembled automatically. Hands you the safe command to copy-paste.

Try it: perch --scan -f deploy.perch. See the animated demo above.

I already have bash scripts β€” what's the migration story? Three options, ranked by effort: (1) Wrap β€” write a thin .perch that calls your existing .sh files via the shell op; gain typed args + --help + MCP + web UI + audit log in minutes. (2) Translate β€” perch --import deploy.sh produces a .perch scaffold preserving semantics line-for-line (mostly shell ops to start), reviewable + statically checkable. (3) Rewrite β€” promote each shell op to native ops over time; once nothing needs shell, --no-shell becomes a real fence. Full guide: migrating-from-shell.md.

Is it a build tool or a CLI framework? Both. Same file becomes a Make-style task runner and a Cobra-style typed CLI. Pick the surface (CLI / web / REPL / MCP / binary) that fits the caller.

Are HTTP redirects and SSRF handled? Yes β€” four layered protections, all default-on. Plus a strict host allowlist when you want to pin which domains the script can reach.

Default What it stops
Block private-IP requests + redirects AWS metadata (169.254.169.254), localhost pivot, RFC 1918 pivot
Block https β†’ http redirects scheme downgrade
Cap at 5 redirect hops redirect bombing
DNS-rebinding defense multi-A responses get ALL records checked

--allow-host HOST[,HOST...] (additive, repeatable) layers a strict allowlist on top. Every initial URL AND every redirect destination must match. Patterns: exact (api.github.com), single-label wildcard (*.s3.amazonaws.com), host:port (localhost:8080), IP literal. Composes AND-wise with the SSRF guard.

# Only api.github.com and the docker registry are reachable β€”
# anything else returns "host not in --allow-host allowlist".
perch --allow-host api.github.com,registry.docker.io,*.docker.io deploy

Opt-out flags for the genuine cases: --allow-private-ips, --allow-scheme-downgrade, --max-redirects N, --no-redirects. Run perch help --allow-host for the full story.

Where do I look up what a flag or concept means? perch help β€” auto-generated reference. Three surfaces share the same catalog:

perch help                    # top-level index, grouped by Execution / Authoring / Security / …
perch help --no-shell         # detail on one flag
perch help shebang            # or one concept
perch help shell              # fuzzy match (4 results in this case)
perch help --json             # full machine-readable dump β€” for agents and tooling

Every error message includes a perch help <topic> hint pointing exactly at the right entry: op "shell" is disabled by --no-shell β€” run perch help --no-shell for details. Both humans and AI agents land on the same canonical reference.

How do I install it / how do I run a remote .perch file? Two one-liners.

Install (macOS / Linux / WSL β€” picks the right binary for your platform):

curl -fsSL https://raw.githubusercontent.com/olivierdevelops/perch/main/scripts/install.sh | sh

Run a .perch file straight from a URL (no save-to-disk step), with restrictions you choose:

curl -fsSL https://raw.githubusercontent.com/olivierdevelops/perch/main/scripts/sample.perch \
  | perch --no-shell --no-network -f - hello

-f - means "read the perch source from stdin." Stdin input is treated as untrusted by default β€” shell, subprocess, network, write, and host env-var visibility are all disabled. Grant capabilities explicitly:

# default: nothing dangerous can fire
curl URL | perch -f - run

# I'm okay with this script using shell:
curl URL | perch -f - --allow-shell run

# I'm okay with shell + network + 2 env vars:
curl URL | perch -f - --allow-shell --allow-network --env HOME,API_KEY run

# I trust this pipe completely (it's my own .perch):
cat my.perch | perch -f - --trust-stdin run

Same model Deno uses: deny-by-default, opt-in with --allow-*. Banner shows "πŸ”’ stdin (untrusted): ..." with the exact flags blocking each capability. File input (-f file.perch) is unchanged β€” the deny-by-default only applies to stdin since that's where untrusted scripts arrive.

Can I run .perch files as scripts (shebang)? Yes. perch --init writes a #!/usr/bin/env perch line at the top and sets the file executable. Then ./commands.perch runs the main command; ./commands.perch hello runs hello; everything between just works. Conceptually a .perch file is a script and a structured CLI surface β€” both at once.

$ perch --init
$ chmod +x commands.perch
$ ./commands.perch           # runs the `main` command
$ ./commands.perch hello     # runs `hello`
$ ./commands.perch --help    # lists commands

The shebang line is just a # comment to perch's parser, so it has no effect on parsing.

Is it a cross-platform shell? Yes β€” and that's the point. With ~140 built-in ops (cp, mkdir, gzip, tar_create, http_get, sha256_file, regex_replace, …) you can write a script that runs identically on macOS / Linux / Windows without falling back to bash or cmd. Disable the shell op and you have a pure portable script. See sandbox.md for the "pure" mode design.

Can I see what a command will do before running it? Yes β€” perch --dry-run cmd prints every op with its interpolated args and skips execution; perch --ask cmd is the same plan interactively (y = run, n = skip, a = run all remaining, q = quit). See it in the terminal below.

Can I lock down what a .perch file is allowed to do? Yes β€” composable flags, each naming what it disables:

perch --no-shell --no-subprocess --no-network --no-write deploy
perch --env HOME,PATH,API_KEY deploy        # ${OTHER_SECRET} now errors

--no-shell blocks shell/shell_output/shell_detached/try_shell. --no-subprocess blocks pkg_install/kill_by_name/etc. --no-network blocks every http_*, download, port_*, etc. --no-write blocks every FS-mutation op. --env restricts which host env vars resolve via ${…}. perch --restrictions lists exactly what each flag blocks. The full capability sandbox (FS roots, network host allowlists, --untrusted with permission previews, file-side sandbox block) is designed in sandbox.md.

Who writes the sandbox β€” the author or the user? Both. The author writes a sandbox block in the .perch file as a manifest of intent β€” "this is what I need to do my job." Reviewers audit it; perch --check enforces it statically. The user layers further restrictions at run time (--no-shell, --no-network, --no-write, --env, --allow-*, --untrusted). The runtime enforces the intersection β€” neither side can grant more than the other allows. Same model as Android permissions: app declares, user grants, OS enforces the overlap. Details in sandbox.md Β§2.5.

Do recipients need to install perch? No. perch --build produces a standalone binary. They run that.

Does it work on Windows? Yes. The ~140 built-in ops are Go implementations, identical on macOS / Linux / Windows. Only shell invocations are inherently OS-specific.

What about secrets? perch reads env vars at runtime (${HOME}, ${API_KEY}, …). Don't bake secrets into the file. The static --check doesn't store anything.

Can I extend it with my own ops? Yes β€” perch is a Go library too. Drop a handler into infra/ops/ and register it. But the ~70 built-ins cover most needs.

How is this different from capy? capy is a small transpiler engine. perch uses capy to define its grammar (lib.perch), so the perch DSL is what capy ingests. capy isn't a language; perch is.

β†’ More: faq.md


Where to next


Full documentation

Grouped by what you're trying to do. Each row is one page.

πŸš€ Get started in 30 minutes

recipes.md 22 ready-to-run .perch files β€” Redis, Postgres, devstack, aistack, observe, kubectl helpers, …
getting-started.md Five-minute hands-on tour
migrating-from-shell.md Wrap your existing .sh files β€” three migration strategies
tutorials/01-replace-your-makefile.md Convert a real Makefile to perch
faq.md vs Make / Just / Task / Cobra; the common questions

πŸ› οΈ Author commands (developers)

language.md Every keyword, modifier, and operator
op-reference.md The built-in op catalog (~140 ops)
execution-contexts.md parallel / retry / timeout / sandbox / cache blocks + templates + --report
testing.md perch test β€” sandboxed behavior tests with assert_* ops
requires.md requires β€” file-declared manifest (bins, env, hosts, FS read/write scopes, OS, hash pins)
capability-gating.md Every external op verifies requires before executing β€” full per-op coverage table
lsp.md VS Code / Neovim / Helix / Zed integration
applications.md 22 real applications worth copying

πŸ“¦ Ship as a product

tutorials/02-ship-a-tool.md perch --build deep-dive
tutorials/03-cross-platform-installer.md One installer for three OSes
embedding.md Fat-binary format spec β€” what's inside, how to verify it

πŸ›‘οΈ Adopt at scale (platform / SRE / security)

sandbox.md Capability model β€” env / FS / net / shell scopes, --untrusted, file-side sandbox blocks
requires.md File-declared manifest β€” bins / env / hosts / FS read+write scopes / OS / arch + SHA-256 hash pins; supply-chain provenance built in
capability-gating.md The enforcement guarantee β€” every external op checks the manifest before it runs, every time; per-op coverage table + regression test
llm-control-plane.md Replace your LLM-tool backend with a .perch file
mcp.md MCP server reference (JSON-RPC over stdio)
applications.md 22 patterns; many are SRE / platform-team shaped

πŸͺž Background reading (design intent)

os-in-a-program.md The "operating system you can scp" framing
user-experience.md UX roadmap
ai-assisted-authoring.md Notes on agent-authored .perch files

Source on GitHub: olivierdevelops/perch. Apache-2.0. One Go binary, no SaaS, no telemetry.