Provision
Execute environment setup for a project from its spec's environment contract — CLI logins, dev-resource provisioning, keys into the host env store, permission allowlist seeding, dry-run — ending only when the preflight passes.From its SKILL.md
npx -y skills add mckissinger/keel --skill provisionAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
22.9 KB, as published. Nobody here has run it
Provision
Set up the project environment from the environment contract in specs/01-architecture.md. This is attended setup, not a milestone: its purpose is to drain every would-be mid-run stall — logins, keys, permission prompts — while the user is at the keyboard. Don't end the sitting until the preflight passes.
The pass
-
Read the environment contract. Every step below derives from it — the contract names this project's services (the examples below, Supabase/Stripe/Vercel/an AI provider, are the common stack, not a required set). If a service in play isn't in the contract, update the contract first (and add a
decisions/entry if the addition is material) — never provision off-spec.Provision every decided service in the contract now — including ones first consumed by later milestones. The user's build runs span multiple milestones unattended, so a service first needed mid-run has no attended launch moment; it must already exist. Defer only two things, marked as such in the contract: services not yet decided (a TBD row — provisioning one converts an open decision into an accident), and secrets that can only be derived at runtime (e.g.
stripe listen --print-secret— record the derivation command instead). -
CLI logins first. Each contracted service's CLI login (e.g.
supabase login,stripe login,vercel login,gh auth login) as needed — have the user run interactive/browser flows with the! <command>prefix. Never ask for, and never accept, a token or password pasted into chat — if a CLI offers a token flow, the user runs it in their own terminal (e.g.! gh auth login --with-token < token.txt), not by handing you the value. Credentials stored per-tool mean later runs never handle raw secrets. -
Provision dev resources. Where the host offers managed integrations (e.g. Vercel Marketplace via
vercel:bootstrap) they auto-inject env vars; otherwise create dev-dedicated resources via CLI — a dev database project, test fixtures. Manual keys (e.g. a spend-capped AI-provider key) go into the host env store, then pull a local copy (e.g.vercel env pull .env.local). Do not paste the key value here — the agent never sees it. The user sets it themselves, in their own terminal, with the exact command for the store in play:gh secret set OPENAI_API_KEY(GitHub Actions), or the service equivalent —vercel env add OPENAI_API_KEY,supabase secrets set OPENAI_API_KEY,stripekeys via the provider console. Each prompts for the value in the user's terminal; your role is to name the variable and confirm it resolves by name afterwards. The host env store is the source of truth;.env.localis a derived copy. Record the one-command recipe that re-derives the local env file in the environment contract (e.g. the exact host-env pull command,vercel env pull .env.local) — committed there, it lets any fresh checkout, worktree, or session regenerate the file mechanically instead of improvising. Author and record the env name-check command in the same pass — the one committed command that asserts every contracted env-var name resolves (host env store or local env file), reporting names/status only, values never in its output; stack-appropriate implementation (a package script, the stack's own env-schema validation, a small committed script), the command recorded, the implementation never mandated. It is the sanctioned path for every session's env assertion — sessions never read.env*files directly. -
Seed the project allowlist — keel's process-verb baseline first, then the contract's services. First merge the
.permissions.allowarray of${CLAUDE_PLUGIN_ROOT}/references/allowlist-baseline.jsoninto.claude/settings.json(committed) — keel's own git + gh workflow verbs, copied never re-authored, so keel's plumbing stops drawing a permission prompt every feature (it names no stack command, so it stays stack-neutral; push-to-default and every merge remain gated by the merge-guard hook, which the baseline never loosens). Then add allow rules for each contracted service's dev-loop commands on top (the entries below are examples of the pattern; the contract's services determine the actual set):- a backend/DB CLI → e.g. Supabase:
Bash(supabase db *),Bash(supabase migration *),Bash(supabase functions *),Bash(supabase gen *),Bash(supabase secrets set *) - a payments/webhook CLI → e.g. Stripe:
Bash(stripe listen *),Bash(stripe trigger *),Bash(stripe products *),Bash(stripe prices *) - MCP tools a run will use get their own entries (e.g.
mcp__trigger__deploy). - Spend commands backed by a spend-capped key (evals, paid API calls) get allowlisted so milestone acceptance gates don't block mid-run —
Bash(pnpm eval:*),Bash(tsx scripts/eval*). Safe only because the key is capped (see step 5).
Never allowlist: resource creation (
supabase projects create,vercel integration add,vercel domains *), prod deploys, live-mode anything,gh repo create,gh api, secrets management beyond the contract, tunnels (ngrok,cloudflared), or machine-level installs. Those prompts are deliberate checkpoints, and they cluster at sittings where the user is present anyway.The allowlist is safe only because the environment holds test-mode credentials — it and the spec's test-key rule are a paired system. Never widen one without re-checking the other.
Seed
specs/run-command-inventory.txtalongside this allowlist — same step, same sitting. It holds every command shape an autonomous run will execute, one bare command shape per line (e.g.pnpm test,supabase db push,gh pr merge 5 --auto; blank lines and#comments are ignored), derived from the same contract + baseline you just allowlisted. This is the filescripts/check-auto-preflight.shhard-requires: the preflight fails closed when it is missing, dry-runs every line against the committed allowlist (a shape no allow rule covers is a launch-blocking gap), and rejects anygh pr mergeline that is bundled/chained rather than bare. Seeding it here — while you're already enumerating the run's commands — is what makes the preflight's dry-run meaningful rather than vacuously green.Seed keel itself in the same committed file, so every collaborator's session has it. So that anyone who opens the project gets keel's skills and hooks with no manual install, this same
.claude/settings.jsonalso carries keel's marketplace + plugin —extraKnownMarketplaces(keel's GitHub source, repomckissinger/keel) andenabledPlugins(keel@keel):{ "extraKnownMarketplaces": { "keel": { "source": { "source": "github", "repo": "mckissinger/keel" } } }, "enabledPlugins": { "keel@keel": true } }Optional: wire the pin gate as a Stop hook. A project may add a Stop hook (or a goal condition) that runs
BASE_REF=origin/main scripts/check-verified-pin.sh, so a session can't quietly end with un-pinned code drift on a code-PR branch — a local convenience on top of the CIverified-pinjob, optional, never mandated (keel is a methodology, not a harness config; the server-side gate is the real one). Three traps if you do wire it: (1) a Stop hook that means block must exit 2 or emit a{"decision":"block"}JSON wrapper — a bareexit 1(what the script returns on failure) is treated as a non-blocking error, not a stop; wrap it. (2) Scope it to code-PR sessions — firing the pin gate on a plan-only or non-repo session is noise. (3) The gate's own message text must not contain the stringcheck-neutralguards against — a goal-condition string that trips the neutrality denylist would fail CI; phrase the condition without the denylisted token. - a backend/DB CLI → e.g. Supabase:
-
Pre-authorize bounded spend — the cap is the authorization. Some milestones' done-conditions need a real-resource action that costs money (an accuracy-gate eval calling a live AI API, a paid API call). In an unattended multi-milestone run these become unsatisfiable mid-run if they require per-use human permission — the run either blocks or defers the gate and stacks later work on unverified code. Drain that at kickoff exactly like a credential. The mechanism: provision a spend-capped dev key (the provider key gets a hard dollar ceiling in its console), then allowlist the command that spends against it (
Bash(pnpm eval:*),Bash(tsx scripts/eval*), etc.). The capped key's value never enters chat — the user creates it in the provider console, sets the cap there, and stores it from their own terminal (gh secret set ANTHROPIC_API_KEY, or the store's equivalent from step 3); you only confirm the name resolves. The cap bounds the worst case including a runaway loop; within it, the action runs unattended without a permission wall. Stop treating capped-key spend as gated — it's pre-authorized by the cap, the same way test-mode payment keys let payment flows run. Size the cap to expected run cost × a small multiple, recorded in the contract. -
Bake the known-flake mitigations into config, not into people's heads. Recurring environment flakes get their mitigation written into the project's config/test setup at provisioning, so a run never rediscovers them — the profile's shared-singleton answer (Q3–Q6) names which apply. The standing shapes: one local backend stack at a time (concurrent unisolated stacks exhaust shared ports/gateways and throw intermittent errors — e.g. a Supabase Kong 502; the scar's mechanism is the sharing, exactly what Q13's per-instance identity removes, so concurrent instances per a proven Q13 contract are the carve-out — the two-instances proof below is itself the per-project test of whether this mitigation lifts, and single-stack stays the default for every non-proven case) and serial local test runs (the test runner's
--workers=1-equivalent for anything touching a shared local stack). A mitigation that lives only in averified:caveat or an agent's memory will be re-hit every run — encode it where the run will actually obey it (test config, preflight, allowlisted command flags). Add new recurring flakes here as identified. For each mitigation that carries a remembered precondition — an env export, a datastore reset/clean step, a required key-prefix — author a committed, executable preflight script that sets the precondition up, asserts it, and fails loud, and record the fragile gate → its script path in the profile's fragile-gate declaration (Q12). A written-down step is not encoding: only the committed script makes the run obey it. The declaration accretes like the signature table — any further fragile gate discovered later, including one surfaced by step 7's ladder run in this same sitting, adds its script and its row when found. Optionally, a lint/typecheck-on-edit hook is a legitimate mitigation for the cheapest-rung class (it catches shape errors at write time) — optional, never mandated; keel is a methodology, not a harness config. Pin tool-setup action versions in CI — never@latest/version: latestfor a setup action that resolves a release over the network: the resolve hits a registry/API that can rate-limit and fail the job spuriously with a red that looks like a code failure (e.g. asupabase/setup-cli@latestbuild dying on "rate limit exceeded"). This step also finalizes the profile's Q12 substrate contract — the parts the derivation recorded as "finalized at provision": assign the project's unique port block / project identity by the name-derivation rule (hash the project name into a fixed band below the lowest OS ephemeral-port floor of 32768 — effectively-random so no/etc/servicesentry claims it, derivable so every project gets its own with no registry or drift, and never transiently taken by the OS), record the environment-target classification (which target the env points at — the local-stack URL/port signature vs the hosted-dashboard URL) and the dual-update caveat (changing the port means updating both the local stack config, which needs a restart, and the hosted dashboard when the env points there), record the canonical invocation path, the env re-derivation command, and the env name-check command inspecs/stack-profile.md, author the stack-hygiene check (Q12 — the committed machine-wide enumeration that classifies every running local-stack instance as this project's or foreign and names each foreign stack's teardown command, reporting only, never tearing down; run it once green here), seed the known-failure-signature table with the known shapes above, and run each singleton's one-line health check green once before the sitting ends (the per-suite duration budgets are recorded from step 7's prove-the-ladder-once timings). And it asks — never assumes — the profile's Q13 isolation contract: the user opts in, or the answer stays "not provided — runtime-proof stays serial". When opted in, author the recipe parts (instance identity, datastore recipe, per-instance env derivation, teardown, allocation) and run the two-instances proof once green — stand up a second instance alongside the primary, both health checks green simultaneously, a write in one invisible to the other, then tear the second down — and recordproven at <date>in the profile. A contract authored but not proven is recorded as unproven and changes nothing. -
The preflight also proves the feedback ladder: run each declared rung (the profile's Q11) green once — the static check, the unit suite, the component-render suite, the e2e driver, whatever the profile declares. This proves the tiers exist and work before any milestone builds against them (the coverage gate in
${CLAUDE_PLUGIN_ROOT}/references/milestones-and-verification.md§3 assumes they do — a missing rung discovered mid-run is this step's failure). A scaffold-fresh suite passing trivially is fine; the point is the command runs and exits green.Dry-run every env-gated rung in both states — armed and unarmed — as part of proving the ladder. Armed (the gate's key name resolving via the sanctioned injection path below): its tests execute — executed count > 0, never "passed" over a silent skip. Unarmed: they skip loudly, naming the unmet gate in the output. A rung that cannot be armed here is a spec-vs-posture contradiction to resolve now, at the keyboard — not mid-run. Author the sanctioned injection path in the same pass: a committed, allowlisted runner (a
with-env-style wrapper that parses, never sources, the derived env file and injects only the named vars) is the one way a session arms an env-gated suite — sessions never read.env*directly (step 3's name-check rule). Without a sanctioned path the live rung is structurally unreachable for an agent, so it silently skips through every verification while reports read green — the scar this step exists to prevent. -
Finish on a green preflight — one cheap read-only command per contracted service (e.g.
stripe products list --limit 1,supabase projects list,vercel env ls). This single step does three jobs: proves each credential is live, surfaces any permission prompt a future run would hit (allowlist it on the spot, attended), and confirms every var in the contract exists (names, never values). These are CLI-level checks needing no app code, which is why they work even for services no milestone has touched yet. Prefer a minimal real call over a no-spend dry-run when the goal is proving credentials work — a no-spend mode validates harness logic but not that the key is live; one cheap real call (a single eval packet, not all ten) catches a rotted key for one token's cost. Verify the repo guardrails are live, not assumed —gh api repos/<owner>/<repo>/branches/main/protectionmust return 200 with require-a-PR + the CI status check required (configure it attended on the spot if it 404s); branch protection is what makes "the agent never commits to main / nothing autonomous crosses go-live" enforced rather than asserted, and the project CLAUDE.md must not claim it until this check passes (an earlier run asserted it project-wide whilemainwas actually unprotected). A green preflight is the exit condition of the sitting.
Unattended-run posture (auto mode, never skip-permissions)
For a multi-milestone run that proceeds while the user is away, the permission posture is auto mode (Claude Code's classifier-gated automatic approval, a research preview) — never --dangerously-skip-permissions. Auto mode approves the clearly-safe calls a run makes back-to-back while still walling the dangerous ones; skip-permissions removes the wall entirely, which is exactly the guarantee keel's gates depend on. Record the posture in one generated-CLAUDE.md line: "Unattended runs use auto mode; never --dangerously-skip-permissions. A blocked call is a stop-point — surface it, don't route around it."
Auto mode complements, never replaces, the two things that actually keep an unattended run safe: the committed allowlist (step 4 — it pre-approves the dev-loop commands so denials are rare and auto mode rarely has to judge) and the human merge gate (the user still merges every PR). Auto mode's classifier has a measured ~17% false-negative rate (it will wave through some calls it should have stopped) — that residual is precisely why the merge gate stays human and why nothing autonomous crosses go-live: the classifier is a convenience layer over the allowlist, not a substitute for the server-side gate. A run that hits a genuine block is a stop-point, surfaced to the user, never worked around by widening permissions.
Auto-provision envelope (an autonomy-mode run)
Under an active keel:auto mode (the doctrine carve-out in decisions/2026-07-05-autonomy-modes-v2.md + decisions/2026-07-genesis-envelope.md), the run is headless with no user at the keyboard — and a headless auto run aborts after repeated classifier blocks (3 consecutive / 20 total), so a single undrained prompt or missing credential doesn't pause the run, it kills it. That is why provisioning for an auto run is stricter than the attended posture above, in four shapes:
-
A user-level provisioning profile. The contracted CLIs are pre-authenticated at the account/org level — scopes sufficient to create project resources non-interactively — and the user maintains a standing library of sandbox/test env vars for the common services (test-mode payment keys, a spend-capped AI key, a sandbox database URL). A new project's contract is then satisfiable from the profile without a mid-run interactive login, which no longer has an attended moment to land in.
-
Secrets flow machine-to-machine, or via the user's
!terminal — never through agent context. Keys move provider → host env store by CLI or by the user typing them with the!prefix — the exact shape is the user runninggh secret set NAME(orvercel env add NAME/supabase secrets set NAME) in their own terminal and answering its value prompt there, never pasting the value into this conversation; the agent's role is limited to existence checks by name — it never reads, relays, or echoes a value. Anything that has passed through agent context is no longer a secret. -
Attended swap-after. Where a project-specific value can't be pre-staged, the run proceeds on a placeholder/sandbox value from the standing library and the user swaps in the real value after the run — the attended sitting is inverted from setup-before to cleanup-after, instead of interrupting the run.
-
The never-allowlist carve-out. Step 4's never-allowlist stays the default. For auto runs only, resource-creation verbs (the
projects create/integration addclass) may be pre-authorized as an explicit provision envelope recorded in the project'sdecisions/— which verbs, for which services, and why — with the spend caps of step 5 as the backstop bounding the worst case. No recorded envelope, no carve-out: an unenveloped creation verb stays a deliberate checkpoint, and in an auto run that means the work waits for an attended moment.
The envelope's exit condition is mechanical, not judged: an auto run launches only after scripts/check-auto-preflight.sh passes. The shipped preflight (canonical in keel's scripts/, copied into the project like the pin gate, and recorded in the scripts/KEEL-SYNC manifest like every shipped-script copy) dry-runs the run's committed command inventory (specs/run-command-inventory.txt, seeded alongside the step-4 allowlist) against the committed .claude/settings.json, asserts the required branch-protection checks — the verified-pin job, the plan-lint job, and the security-review job — are actually required on the default branch, asserts the security-review check is backed by workflow content that performs a review (check (b2): one .github/workflows/ file declaring the context, invoking the review implementation on an uncommented uses: line matching the pattern, and triggering on pull_request — PREFLIGHT_SECREVIEW_PATTERN for a different in-Actions implementation, PREFLIGHT_SECREVIEW_EXTERNAL=1 as a loud, echoed attestation for a non-Actions provider), and confirms every env-var name in the architecture contract resolves in the host env store (names only, never values). All three checks are asserted at arming; they are wired in two tiers — verified-pin + plan-lint at kickoff (spec-foundation / adopt repo setup), security-review at auto-entry: genesis wires it at bootstrap, and on a project stood up attended auto:feature/auto:run wire it as preflight remediation when the gate names the gap (the tier's job set and the recorded default implementation of each live in ${CLAUDE_PLUGIN_ROOT}/references/template-contract.md tier 1). The tier split is about who wires what, when — never about which checks the preflight demands. It fails closed, including when gh or jq is missing. Add one line to the generated CLAUDE.md next to the posture line above: "Auto runs launch only after scripts/check-auto-preflight.sh passes; a preflight failure is fixed attended, never worked around by widening permissions mid-run."
Later additions
A service that becomes decided after kickoff (a TBD row resolved, or a new contract entry) gets this same pass in miniature at the next attended moment — a run launch — never mid-run. Before any multi-milestone run, the preflight covers every service any milestone in the run's scope consumes, not just the first.