moor runs AI coding agents in a sealed box on your Mac.
Each project gets its own Docker sandbox. The agent (Claude Code) and the pipeline that plans and checks its work run inside it, with no access to your disk and no way onto the internet except through an allowlist. moor watches from outside and keeps a tamper-evident record of what happened.
Always know the next step. A feature moves through seven steps: write
the spec, check it, approve it, plan it, check the plan, approve it, build
it. You write the spec on your Mac, with whatever help you like; after that,
moor next tells you where you are and the one command that moves you on,
and those commands never need a spec name.
moor spec new login && moor spec push login.spec.md
moor next # my-app · login · step 5 of 7: approve the plan
moor approve # shows the plan, then asks y/N
moor go # runs the next steps until one needs youLet an agent loose without risking your machine. A misbehaving agent, or a malicious repo it clones, can wreck at most one throwaway container, never your Mac or its other projects.
moor new my-app --image moor/node:latest
moor ask -p my-app "add rate limiting"Stop code or secrets leaking out. Everything leaving the sandbox goes through a proxy with an allowlist (GitHub, the registries you name). Every request is logged, including the blocked ones.
See exactly what the agent did. Every command, every connection, every push (with the commit it pushed), and every check result and approval lands in one hash-linked record on your Mac, where the sandbox can't touch it.
moor audit my-app # the trail
moor audit my-app --verify # has anyone edited it?Hand over proof, not a story. moor bundle packs a run's evidence and
moor's record into one file, and verifies it before you send it.
moor bundle -p my-app --out /tmp # ✓ bundle pass → moor-my-app-….tar.gzDrive a whole feature from one description. moor recipe takes a loose
description and runs the spec → plan → build pipeline inside the sandbox,
stopping for your approval at every checkpoint.
| What you get | |
|---|---|
| Guided workflow | moor next shows the step you're on and the command for it; moor go, moor approve and moor reject act on the active spec, so you never type a spec name or stage. |
| One record, one format | moor writes its audit trail in the same hash-chained format the pipeline uses, so one verifier checks both. Old trails are sealed, not rewritten. |
| Sandbox attestation | On moor up, moor inspects the running container and hands the pipeline a checked list: read-only, non-root, no capabilities, no mounts. The pipeline can refuse to run an agent without it. |
| Check results in the record | Nothing inside the sandbox writes the record itself; moor collects the pipeline's entries and appends them from outside. |
| Pushes you can trace | git push via moor run records the branch and the exact commit. |
moor bundle |
A verified evidence bundle carrying moor's record, built outside the sandbox. |
| Real names on approvals | Approvals record your git identity, not "unknown". |
| Pinned tooling | Rebuilding images installs exactly the pipeline version you pin. |
What moor is built from, and how the pieces fit, is in docs/ARCHITECTURE.md.
Every project gets its own Docker sandbox with no host bind mount, no
docker.sock, and a default-deny egress proxy. The agent (Claude Code
today) and its pipeline both run entirely inside the container, so a
misbehaving agent or a malicious cloned repo can compromise at worst one
throwaway container, never the Mac it runs on.
See docs/THREAT-MODEL.md and
docs/ARCHITECTURE.md for the full design and the
components moor is built from,
docs/MCP.md for the transport in both directions — how moor
puts an instruction in front of the agent, and how the agent reaches the
pipeline's checks through an MCP server that deliberately has no approve
verb in it —
docs/IMAGES.md for what is in each image, what is pinned
and what isn't, and the build mechanics that are easy to get wrong,
docs/WALKTHROUGH.md for a real ideation-to-shipped
run against an actual GitHub repo — including the three bugs it found —
docs/MIGRATING.md for bringing an existing project
into a sandbox, and
docs/decisions/0001-container-runtime-choice.md
for why this stays on runc rather than gVisor or Apple's native
container tool, and
docs/decisions/0002-claude-code-authentication.md
for why Claude Code auth is an injected token rather than a mounted
~/.claude,
docs/decisions/0003-claude-code-permissions.md
for why every project defaults to Claude Code's own auto permission
mode instead of --dangerously-skip-permissions,
docs/decisions/0004-rename-to-moor.md
for why this was isolator and is now moor,
docs/decisions/0005-recipe.md for
moor recipe — driving the spec/plan/build pipeline from a
loosely-described outcome, stopping for your approval at each checkpoint —
docs/decisions/0006-recipe-logs.md
for moor logs — a live, timestamped status view of a running recipe
from any terminal, not just the one driving it —
docs/decisions/0007-git-identity.md
for why the sandbox's git identity is synced from the host's, so
approvals record a real name instead of "unknown",
docs/decisions/0008-agent-session-protocol.md
for why the agent reaches the pipeline through moor's own MCP server rather
than a scoped shell with a hand-maintained verb allowlist — and
docs/examples/ascii-banner for a small
utility built end to end by a real Claude Code agent running inside a
sandbox — including two real bugs that run found and fixed, and the
full security/audit verification against the live container.
docs/CI.md covers what runs in .github/workflows/ci.yml
on every push/PR — Rust lint/test/audit/deny, shellcheck, hadolint,
gitleaks, a Trivy CVE scan of every built image, and the real
tests/e2e.sh against live containers — and, importantly, what each of
those actually found and fixed versus what's deliberately (and
narrowly) suppressed, with the reasoning inline.
# one-time: build the sandbox + egress images
./images/build.sh
# one-time: build the moor CLI
cd cli && cargo build --release
# (or use ./target/debug/moor during development)
# create a project (add --github to also create a private GitHub repo)
moor new my-app --image moor/node:latest
# ...or bring an existing project in — history and all, no bind mount
moor import my-service --from ~/Repos/my-service
# make it the default, so later commands don't need its name
moor use my-app
# write a spec on this Mac (by hand or with your AI assistant), then send it in
moor spec new greet-name # writes ./greet-name.spec.md, rules at the top
moor spec push greet-name.spec.md # sends it in, checks it, makes it the active spec
# where does the active spec stand, and what's the one command for it?
moor next
moor next --all # every spec and its step
moor use my-app --spec greet-name # pin which spec `moor next` follows
# the commands it points you at, which always act on that spec
moor go # take the next steps until one needs you
moor go --check # re-check a build you fixed, without the agent
moor approve # shows the spec/plan/run first, then asks
moor reject "needs a rollback plan"
moor ship # approved: commit on its own branch, push, open a PR
# read a spec, or its plan or tasks, without opening a shell
moor view greet-name tasks
# talk to the agent, or drop into the sandbox yourself
moor ask "why is the login test flaky?"
moor shell my-app
moor run my-app -- npm test
# Commands that take --project work out which project you mean: the flag,
# then `moor use`'s default, then the one sandbox that's up — and only ask
# you to choose if none of those settles it.
# check the sandbox is actually locked down the way it should be
moor selftest my-app
# see what's happened in this project so far
moor audit my-app
# an evidence bundle for the latest run, carrying this trail, verified
moor bundle -p my-app --out /tmp
moor down my-appSecrets (ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN, GITHUB_TOKEN,
...) are read from your shell's environment if exported, otherwise from
the macOS Keychain — set one once and every future moor up just
picks it up, nothing to re-export each session:
moor secrets set my-app ANTHROPIC_API_KEY # prompts, hides input where possible
moor secrets status my-app # where each declared secret resolves from
moor secrets unset my-app ANTHROPIC_API_KEYIf you pay for Claude via a claude.ai subscription rather than metered
API credits, use CLAUDE_CODE_OAUTH_TOKEN instead of ANTHROPIC_API_KEY
— the claude CLI inside the sandbox honors it the same way. Generate it
once on the host (this does the interactive login there, not in the
container) and store it the same way as any other secret:
claude setup-token # one-time, on the host — opens a browser login
moor secrets set my-app CLAUDE_CODE_OAUTH_TOKEN # paste the token it printsNote: whether the build step runs the agent (rather than falling back to a
no-agent mode) is decided by the pipeline's own credential check inside the
sandbox, not by moor. If it doesn't pick up CLAUDE_CODE_OAUTH_TOKEN the
way it picks up ANTHROPIC_API_KEY, see
docs/ARCHITECTURE.md.
Taking a spec through every step by hand — write it, check it, approve it,
plan it, check the plan, approve it, build it, approve the merge — gets
old, even with moor go. moor recipe drives that whole sequence from
one loosely-described outcome, stopping at the same checkpoints for you:
moor recipe my-app docs/examples/recipe/greet-function.recipe.mdThe recipe file is just YAML front matter (slug, scope) followed by
free text describing what you want — not a DSL, deliberately: the free
text goes to the agent close to verbatim, so writing it loosely is the
intended way to use this, not a limitation of it. The recipe's spec
becomes the active one, so moor next and moor approve follow it.
Every run stops and tells you exactly what to do whenever a decision is actually yours:
PAUSED for your approval. Review it and decide:
moor approve (shows it first, then asks)
moor reject "why"
...then re-run this recipe to continue.
Content-authoring checks (spec/plan) get a bounded, tool-restricted
self-correction loop — a failing check's own output is fed to an agent
that can only use Write/Edit, never Bash, so it can fix exactly what
the check named without going and implementing the feature instead. The
actual build gets no such auto-retry beyond a small attempt cap — that
step has full tool access, and iterating on it unattended is exactly the
scope creep this project's security posture argues against, so it stops
and hands the evidence to you instead. See
ADR-0005 for the full design, and
docs/examples/recipe for a real run's output —
including two real bugs this exercise found, with fixes.
Since the build step can take a while and a recipe's own progress
narration used to only go to the terminal that launched it,
moor logs <name> [--follow] reads the same tamper-evident audit trail
and shows just the critical events — stage transitions, check attempts,
pauses for approval — from any terminal, live:
moor logs my-app --follow[17:31:07] [logs-verify] stage: spec
[17:31:07] [logs-verify] gate attempt 1/4: fail
[17:31:14] [logs-verify] gate attempt 2/4: fail
[17:31:25] [logs-verify] gate attempt 3/4: pass
[17:31:25] [logs-verify] stage: spec_approval
[17:31:25] [logs-verify] PAUSED — waiting on: spec_approval
See ADR-0006 — verified live, watching a second terminal pick up real stage transitions and gate failures within half a second of a real recipe run producing them.
images/— the sandbox base image + per-language layers (node, rust, python) and the opt-incopilotagent layer; see docs/IMAGES.mdmcp/—moor-keel-mcp, the MCP server that ships inside the sandbox and is the agent's only route to keel; see docs/MCP.mdproxy/— the egress gateway (default-deny forward proxy)policies/— the hardening baseline and the manifest schema, documentedcli/— themoorRust CLI, includingcli/templates/(the per-project docker-compose template — lives inside the crate, not a top-levelcompose/, so a published crate can actually embed it)docs/— threat model, architecture, transport, imagesMakefile— local dev tooling (make help); mirrors.github/workflows/ci.ymlsomake ciruns the same checks locally
Nothing here asks you to take moor's word for it — every claim below is either checked by code you can read, or was verified live against a real container and written up with the evidence attached:
- The core guarantee is structural, not configurable. No host bind
mount, no
docker.sock,cap_drop: ALL, non-root, read-only rootfs, a genuinely internal Docker network with no route out except through the egress gateway — see docs/THREAT-MODEL.md for why each one is there.moor selftestre-checks all of it against the live container (not just the manifest) every time you run it, and fails safe: a corrupted or missingdocker inspectfield reads as FAIL, never as "all clear" (missing_fields_default_to_failing_safeincli/src/commands/selftest.rs). - The audit trail is tamper-evident, not just append-only. Every
entry hash-chains to the one before it;
moor audit --verifyrecomputes the whole chain and names exactly which entry was edited, deleted, reordered, or forged if any was — proven with a live tamper test intests/e2e.sh, not just asserted. The chain is keel'skeel.chain/1format, so keel's ownkeel chain verifychecks it too, and it holds keel's gate verdicts and the sandbox's posture attestation alongside moor's own entries: one chain, not two logs to reconcile. - The agent's own permission checks are a second layer, not skipped.
Every project defaults to Claude Code's
autopermission mode (images/base/claude-settings.json) instead of--dangerously-skip-permissions— verified directly against a live container: it creates files when asked and refuses a destructiverm -rfunprompted, with no human needed to answer a prompt. See ADR-0003. That means a compromised or manipulated agent run (the real risk category incidents like prompt-injection-driven tool abuse fall into) has to get past two independent things, not one: Claude's own per-call risk judgment, and the sandbox's structural containment below if that judgment is ever fooled. - The agent cannot advance its own work past a human checkpoint — and
the first attempt at that claim was wrong, which is documented rather
than quietly fixed. The agent reaches keel through an MCP server moor
ships into the sandbox (docs/MCP.md), exposing
keel gateandkeel nextand nothing else;keel approveis not a tool it defines, and every argv the server can produce is a compiled-in verb plus at most a validated slug and a gate id matched against a fixed table — asserted three ways inmcp/src/main.rs, including over every argv any accepted call can produce. But that alone did not hold:--allowedToolsturned out to be an auto-approval list, not a restriction, so Claude Code ranBashregardless and thebuildrole reachedkeel approve --helpthrough it. Every role now also passes--disallowedToolsfor every route to a shell or a subagent, which is what actually denies it — verified live both before and after. The honest accounting, including that this makes the control half list-based and therefore something moor must keep current, is in ADR-0008's correction. - An agent's reply cannot address your terminal. The sandbox
constrains what the agent can execute, not what it can say — and a
reply drawn straight into a terminal can move the cursor, rewrite lines
already read, or drive an OSC handler. Everything
moor studiodraws from a turn is stripped of every ANSI/CSI escape, every OSC sequence, and every C0 control except newline and tab (\rincluded) first. The renderer is a pure function, so what it would draw is asserted on directly in tests rather than eyeballed. - Every conversational turn is chained before you see it, and the chain
holds no conversation.
moor ask/moor studioappend oneagent-turnentry — role, granted tool set, session id, SHA-256 hashes of prompt and response — before printing anything, so there is no code path to a response that skips the record. Text is deliberately not in the chain:moor audit --exportbundles that file wholesale, so it goes to the redactedtranscript.jsonlinstead. Session continuity is the host's: the id is stored only after it validates as a UUID, and the agent's own response text is never parsed for one. - Every design decision that matters is written down, including the
ones that didn't go moor's way. ADR-0001 explains why gVisor and
Apple's
containertool were both rejected (and exactly what would need to change for that to flip); ADR-0002 explains why Claude Code auth is an injected token rather than a mounted~/.claude, including a proposal that was made and rejected during review. Nothing about the threat model is asserted without the reasoning attached. - CI enforces this on every push, and was itself verified for real —
not just written and assumed to work. docs/CI.md covers
Rust lint/audit/deny, shellcheck, hadolint, gitleaks, a Trivy CVE scan
of every built image, and the full
tests/e2e.shbattery; every job was run against real GitHub Actions before being called done, and the doc names the real bugs that surfaced doing it (a broken Bash tool under the read-only rootfs, a silently-corrupted secret from a triple-paste, an end-of-life Node runtime) rather than just the tools' names. - The repo is public. The threat model, every hardening control, and every known gap below are readable by anyone, including someone deciding whether to attack it — the design has to hold up without relying on obscurity, and that's a deliberate choice, not an oversight.
Being direct about what isn't solved is part of the trust case above, not separate from it:
- No branch protection on
master. CI reports pass/fail; nothing currently blocks a push that fails it. Low-stakes for a single- operator repo today, but worth fixing before this has other committers. - No scheduled re-scan. Trivy/
cargo audit/cargo denyonly run when code changes (push/pull_requesttriggers) — a CVE published against an already-built, unchanged image or dependency tree goes undetected until the next commit, which could be a long time. - No vulnerability disclosure process. Public repo, no
SECURITY.md— there's no documented way for someone who finds a real issue to report it privately instead of opening a public issue. - The shared-kernel risk is open, not mitigated. Staying on
runc(ADR-0001) means a kernel/runc syscall escape isn't defended in depth the way gVisor or per-container VMs would; the only current mitigation is keeping Docker/OrbStack current. gVisor was rejected because this platform can't run it today, not because the risk it addresses isn't real. - Debian CVEs with no fix yet stay in every image, indefinitely.
trivy --ignore-unfixedis the right CI gate (see docs/CI.md for why), but it deliberately doesn't fail onaffected/fix_deferred/will_not_fixfindings — real, currently-unpatchable exposure that the scan will never turn red for. - Secret values are briefly visible in one subprocess's
argv.security add-generic-password -w <value>(macOS Keychain writes) has no non-interactive API that avoids this — documented as an accepted tradeoff incli/src/secrets.rs, not something moor's own code can route around. - No image signing, provenance, or SBOM. Nothing today lets you cryptographically verify a built image matches this source, or hand someone a machine-readable bill of materials for one.
- Auto mode's risk judgment is Anthropic's to maintain, not moor's
to verify per-release. ADR-0003
replaced the blanket
--dangerously-skip-permissionsdefault with Claude Code's ownautopermission mode — a real improvement, checked directly against a live container — but that mode's actual judgment calls are a model behavior moor doesn't control and hasn't re-verified across every future Claude Code version. An operator can still pass--dangerously-skip-permissionsexplicitly for a given run if they want the old behavior back. - No independent review. Everything above is self-assessed — this project's own docs, ADRs, CI, and one real walkthrough. No third-party penetration test or external security audit has been done.
- A scheduled (nightly/weekly) CI run of the Trivy/audit/deny jobs, independent of code changes, to catch newly disclosed CVEs against images that haven't otherwise changed.
- Branch protection requiring CI to pass before merge, once this has more than one committer.
- A
SECURITY.mdwith an actual disclosure contact/process. - Image signing (cosign/sigstore) and SBOM generation (syft), published alongside each build.
- Revisit gVisor if a self-managed Linux host is ever justified; revisit
Apple's
containertool if apple/container#719 (the host-gateway egress leak) closes with a real fix. - A custom seccomp profile derived from real
straceoutput of an actual toolchain run, replacing the current default profile (already flagged as deferred inpolicies/README.md). - A larger active breakout battery in
moor selftest— today's three probes (canary domain, read-only fs,docker.sock) are a starting point, not a ceiling. - Automated dependency updates (Dependabot/Renovate) for
Cargo.lockand the toolchain versions baked into each image, instead of relying on someone remembering to bump them. - Re-verify
autopermission mode's actual behavior (does it still create files when asked, still refuse a destructive command unprompted) whenever the base image bumps its Claude Code version — it's a model behavior, not a pinned rule moor controls.
cd cli && cargo test— 129 unit tests: selftest's hardening evaluator (fed syntheticdocker inspectJSON, including fail-safe-on-missing-data cases), manifest validation and round-tripping, compose template rendering, the tinyproxy access-log parser, the audit hash chain (append/verify, plus deliberately editing, deleting, reordering, and forging entries to confirm--verifycatches each one; a golden entry written by keel that moor must hash identically; sealing a legacy chain; folding keel's sink, malformed lines included), the posture attestation derived fromdocker inspect(unproven when a field is missing), push ref and commit capture, Keychain service-name scoping,moor import's image auto-detection and GitHub-URL parsing,moor keel/moor view's project-resolution precedence (explicit flag, stickymoor usedefault, the one sandbox that's up, ambiguity errors),moor keel's argv building,moor use's known-project validation,moor view's artifact-name mapping and markdown highlighting, the agent session protocol (role tool sets,is_error-based turn failure, UUID-validated session continuity, the content-freeagent-turnchain entry, transcript redaction, recipe emission), andmoor studio(one container query per refresh, input handled while a turn is in flight, escape-sequence stripping, the two-distinct-key approval, thedocker exec -iartifact round-trip, per-project session isolation, stage read fromkeel next).cargo test --manifest-path mcp/Cargo.toml— 8 unit tests over the MCP server's tool surface: that it coversgateandnextand that no argv any accepted call can produce reaches an advancement verb, that a slug must pass the same rulemanifest::validate_nameapplies, that a gate id is matched against a fixed table rather than passed through, and that an unexpected argument key is refused rather than ignored../tests/e2e.sh— end-to-end against real Docker containers: creates a throwaway project, runsmoor selftest's static checks and active breakout battery, confirms egress allow/deny against github.com and example.com, confirms agit pushattempt is recorded with its ref, confirms the posture attestation is in the chain and that the agent cannot replace it, folds keel's sink and the egress log in, checks the host chain withkeel chain verify($KEEL, defaultkeel), verifies the chain, live-tampers with the real chain.jsonl file and confirms--verifydetects it, exports an audit bundle, and imports a throwaway local multi-branch repo end-to-end (image auto-detect,keel init, both branches present, selftest still passes). Requires the images to already be built (./images/build.sh).- Both of the above, plus security/quality scanning (Rust lint/audit/deny, shellcheck, hadolint, gitleaks, Trivy image CVE scans), run in CI on every push/PR — see docs/CI.md.
make helplists every local dev target —make checkfor a fast fmt/clippy/test pass,make securityfor the full scan suite,make cito run everything CI runs (including the realtests/e2e.sh) in one shot. Each security target checks its tool is installed and says how to get it (brew install ...) rather than failing on a bare "command not found." Every target here was run for real before being documented, not just written.
master is protected, so a release is two steps, each checked by
release.sh before it changes anything (add --dry-run to see what it
would do):
./release.sh prepare 0.4.0 # opens the release PR: version + CHANGELOG
# merge that PR, then:
git pull --ff-only && ./release.sh publish 0.4.0publish runs the same gates CI does, publishes to crates.io with the
token cargo login stored, tags v0.4.0 (pushing only the tag), and
creates the GitHub release from the CHANGELOG. It refuses to publish from
any commit but the one that matches origin/master, and a version already
tagged elsewhere; every step checks whether it's already done, so an
interrupted release finishes by running the same command again.
make help # list every target
make check # fast: fmt-check, clippy, cargo test — no Docker needed
make security # cargo audit/deny, shellcheck, hadolint, gitleaks, trivy
make ci # everything above plus the real tests/e2e.sh — the full CI replica
make build # cargo build (debug)
make release # cargo build --release
make install # cargo install the CLI to ~/.cargo/bin
make clean # cargo cleanmake publish-dry-run runs cargo publish --dry-run. This crate was
originally named isolator and marked publish = false on purpose —
isolator is already taken on crates.io by an unrelated package, and
the crate wasn't meant to be published at all at the time. Three things
were needed to actually fix that, not just the first one that looked
like the whole story: renamed to moor (available, checked directly
against the crates.io API — see
ADR-0004 for the naming
process and every alternative tried), MIT-licensed
(LICENSE + license = "MIT" in cli/Cargo.toml), and — found only by
actually running cargo publish --dry-run --allow-dirty all the way
through, not stopping at the first passing check —
cli/src/compose.rs's include_str! moved from a top-level compose/
into cli/templates/, since a published crate can't embed a file that
lives outside its own package directory. cargo publish --dry-run now
packages, compiles from the isolated package tarball, and gets to the
upload step (aborted only because it's a dry run) — verified, not
assumed.
make publish runs the real thing. It's still a deliberate manual step
for whoever owns the crates.io account — nothing here runs it for
you — but it exists now: it re-runs publish-dry-run first, then
requires typing the literal word publish at a prompt (not just
pressing enter) before calling cargo publish itself, since a crates.io
version can be yanked afterward but never deleted or reused. Needs
cargo login done once beforehand, with a token from
https://crates.io/settings/tokens. One caveat ADR-0004 is explicit about:
cargo install moor only brings the compiled binary, not images/,
proxy/, or policies/ — without those (from a full
clone, with ./images/build.sh run once) the installed binary has no
sandbox images to build from.
Phases 0–9 of the plan are done, with Phase 9 partial by design (see below):
-
0–3: threat model/architecture docs, base + language images, the egress gateway with a default-deny allow-list, the container hardening baseline (read-only rootfs, dropped capabilities, no bind mounts, non-root user, no default-bridge network).
-
4: the
moorCLI (new,up,down,shell,run,status,audit,selftest). -
5: keel + the Claude Code CLI verified running inside the sandbox (
keel init,keel runall execute there, not on the host). -
6: a hash-chained, tamper-evident audit trail per project (
audit/chain.jsonl) folding in exec commands, egress verdicts, and a distinctgit-pushtag;moor audit --verifyand--export. -
7:
moor selftestnow also runs an active breakout battery (canary-domain reachability, read-only-fs write attempt,docker.sockpresence) against a live container, not just staticdocker inspectchecks. -
8: a real project run end-to-end — a private GitHub repo, a spec, two real gate-caught scope violations, a real
npm install/node --test/node --checkbuild inside the sandbox, a realgit push, and a verified 51-entry audit trail. Found and fixed three real bugs: the private-repo clone had no credentials, the--exportbundle pointed at a keel evidence path that doesn't exist, and (same root cause) the audit command's own help text repeated it. -
9: Keychain-sourced secrets are built (
moor secrets set/unset/status; resolved automatically byup/run/shell, ahead of the operator's own shell env, with redaction wired through so a Keychain-only secret still gets scrubbed from the audit chain — this closed a real gap: it originally worked only within the single process that first resolved the secret, not latermoor runinvocations). Checked gVisor/runsc: not available under this OrbStack install (onlyrunc), and switching to Docker Desktop wouldn't fix that either — its engine runs in the same kind of managed, non-administrable VM. Also looked at Apple's nativecontainertool (its own-VM-per-container model would sidestep the "shared kernel" concern more fundamentally than gVisor does) — not usable on this Mac yet (needs a newer macOS than 15.3.1) and not a drop-in replacement for thedocker composelayer this project is built on regardless. Both fully written up inpolicies/README.md's Runtime rows.Per-task ephemeral containers for
--waves— deprioritized, not just deferred. That item only matters if multiple keel tasks run concurrently in the same container, which only happens withkeel run --waves. Daily usage here is serial (gate g0→approve→gate g1→approve→run, one stage at a time), where only one task is ever active in the sandbox at once — so there's no concurrent-task exposure to close today, and the canary token being per-project rather than per-task is a non-issue when there's only ever one task running. Revisit if--wavesactually gets used.Still genuinely deferred: remote/centralized audit log shipping (no remote destination has been specified to ship to).
-
Migration:
moor importbrings an existing local project into a sandbox — history transferred via a one-shotgit bundle(never a bind mount), image auto-detected, existing GitHub remote preserved. Verified against keel's own repo: exact commit history, both branches, and its already-existing.keel/keel.tomlcorrectly left alone instead of overwritten. Found and fixed two real bugs building it:docker cpsilently fails against a read-only rootfs even onto a writable tmpfs mount (fixed by streaming throughdocker exec's stdin instead), and cleaning up a stale bundle-pathoriginremote was deleting every non-default branch along with it (fixed by materializing branches locally first). -
Recipe:
moor recipedrives keel's spec/gate/plan/gate/run pipeline from a loosely-described outcome, stopping for human approval at the same checkpoints keel already defines. Verified end-to-end on a real spec, gates failing and self-correcting for real (twice, on genuinely different problems each time) along the way — see docs/examples/recipe. Found and fixed three real bugs building it: a concrete goal description reliably makes Claude Code implement the thing instead of writing a spec about it, fixed structurally with--allowedToolsrather than prompt wording;--allowedTools <tools...>is variadic and swallows the prompt itself if the prompt comes after it; and keel's hard refusals (e.g. "already exists") print to stderr while gate results print to stdout, whichcli/src/proc.rs's stdout-onlyrun_capturewas silently dropping — fixed by addingrun_capture_combinedalongside it, not changing its existing callers. -
Recipe logs:
moor logsshows (or follows) a running recipe's stage transitions and gate attempts from any terminal, reading the same audit chainmoor auditalready writes rather than a second logging mechanism. Verified live: a--followin one terminal picked up every stage transition and gate pass/fail from a real recipe run in another, in real time. -
Git identity:
moor up/new/importnow sync the sandbox's git identity from the host's owngit config— tracedkeel approve's "unknown" straight to keel's own source (git config user.name, then$USER, then "unknown"; the sandbox had neither set). Verified live against the realkeelproject:keel approvewent from recording "unknown" to recording "Dane Balia".