metabot
A local bot that runs on the coding-agent plan you already have. Each call it gates is classed read, write or destructive, and it asks you before a write or a destructive call runs. It leaves a signed receipt for every run that anyone can check offline. Which calls it gates depends on the engine, as the table under Engines shows.
Part of Metaorcha. Status: pre-release, built in the open.
- Your engines, your plan. It drives the coding-agent command line tools you already sign in to, or a model server on this machine.
- A gate on the calls that pass through it. Reads run, writes ask, and a
destructive call runs when you type
yes. - A receipt for every run. The receipt is signed with your key, and the run's journal re-derives every hash in it.
- Offices, routines and a web page. It runs a Code Desks office headless, fires routines on a schedule, and shows all of it on a page on this machine.
Install
From a checkout of this repository:
uv tool install ./apps/metabot
It is not on a package index yet. It needs Python 3.11 or newer and a POSIX system, and it is tested on Linux. Install and sign in to each engine you want to use, the same way you do without MetaBot.
Getting started
metabot init # makes a signing key if you have none, and picks a default engine
metabot doctor # checks the engines, the key and the home folder
metabot engines # which engines are here, and which is the default
metabot ask "what is in this directory?"
metabot receipts verify <run-id>
metabot with no command, or metabot chat, opens a chat in this terminal.
metabot ask - reads the prompt from stdin, and --json prints the result as
one object.
Engines
-e |
What runs | Its own tool calls |
|---|---|---|
claude |
the claude command |
a hook asks the gate before each one runs |
codex |
the codex command |
run in the engine's own sandbox, then recorded |
cursor |
the cursor-agent command |
run in the engine's own sandbox, then recorded |
openai |
a server that speaks the chat completions API, at http://127.0.0.1:11434/v1 by default |
run by MetaBot, each after the gate |
-m picks the model. metabot config show lists the settings that
metabot config set changes. A hosted endpoint is set by hand in
config.json, with openai_base_url and openai_model.
Its key stays in the environment variable that openai_key_env names, and
MetaBot reads it at call time and never stores it.
Roles map kinds of work to an engine and a model: fast, build, review,
second and private. metabot roster shows them, metabot roster set build --engine codex changes one, and metabot roster unset build gives it
back its default. metabot suggest TASK names a role, an engine and a model,
with the rule that chose them. metabot ask --role build runs on a role, and
metabot ask --suggest runs on the suggestion. The run's journal keeps what
was suggested beside what ran, and the receipt signs what ran.
Connections and approvals
A connection is an MCP server you add once and MetaBot gates on every run.
metabot connect add gh --token-env GH_TOKEN --command my-mcp-server
metabot connect add docs --url https://example.com/mcp
metabot connect scope gh # each tool's class: read, write or destructive
metabot connect scope gh --set merge_pull_request=destructive
metabot connect test gh # list its tools
metabot connect list
metabot connect revoke gh # deletes its token; it cannot run again
--command takes the rest of the line, so it comes last. --set can make a
tool's class stricter, never looser. scope shows a setting for a name the
server does not list, so a misspelt one is seen.
The token is read from stdin, or asked for, and never taken as an argument. It is kept in its own 0600 file, and it never reaches a prompt, a journal or a receipt. Output that holds it is recorded with the token replaced.
When a call needs your answer, MetaBot asks on this terminal. Type y to
allow a write, or yes to allow a destructive call. Anything else declines.
With no terminal, every ask is declined, and the decline is recorded.
--allow-write runs writes without asking, never a destructive call.
--allow gh__create_issue runs one write tool without asking, and
--read-only runs the engine so it can read but not write.
A connection added with --browser drives a browser. On this machine's own
addresses, and on each --origin you add, a call is read or write by its
name. Anywhere else, or where MetaBot cannot tell, it is destructive. So is
any call that runs code in the page.
Receipts
Every run seals a receipt when it ends, including a run that failed or was
stopped. metabot receipts list shows recent runs, metabot receipts show RUN shows what one did, and metabot receipts verify RUN checks its
signature and that the kept journal re-derives every hash in it. The receipt
also checks offline with orcha verify FILE, a command of the orcha-sdk
package.
The receipt says who or what let each write through the gate. A write you
approved names your key. A write that ran under an allow you set before the
run, with --allow-write or --allow or in a routine, names that allow. A
declined call carries its reason. A read carries none, and neither do the
calls the codex and cursor engines run in their own sandbox.
A model can say a call worked when it was declined or failed. So when a run has such calls, MetaBot says how many under the answer, in the terminal and on the run's page. The steps in the receipt are what happened.
The signing key is at ~/.orcha/key, or where ORCHA_KEY_PATH points.
metabot config set project_receipts true, or --project-receipt on one run,
also copies each receipt into the project, under .orcha/receipts/. The copy
in the MetaBot home is the one that counts.
Offices
MetaBot reads a Code Desks office, office.yaml, and runs its desks headless.
Code Desks' desk init writes one, or you can write it at the top of your
repository:
name: my-office
gates:
done:
owner: [verdict]
engineer: [tests]
checks:
tests: make test # runs with --checks, once you trust it
desks:
build:
role: roles/build.md # the desk's instructions, a file in the repository
harness: claude # claude, codex, cursor, custom or any
second:
role: roles/build.md
harness: codex
local:
role: roles/build.md
harness: any
engine: openai # the model server MetaBot's openai engine uses
review:
role: roles/review.md
harness: codex
advises: [build] # reads build's work when it is done, and changes no file
metabot office check # the desks and checks it reads, and what cannot run
metabot office trust # shows the gate checks, and records that you agree to run them
metabot office run "add a health check endpoint"
metabot office run --fan "add a health check endpoint"
metabot office run --desks build,local "add a health check endpoint" # these desks in this order, and their advisors
metabot desks list
metabot desks pick <office-run>/build
metabot desks clean <office-run>
Each desk works in its own git worktree, on its own branch under metabot/.
In a relay, each desk starts from the branch of the desk before it, and gets
the earlier desks' answers as files. --fan runs the desks at once from one
base, so you can compare their work. A desk with fan: 3 runs as three
copies at once, each on its own branch. This machine's fan_cap bounds how
many desk runs go at once: three unless you change it with metabot config set fan_cap, and five at most. A desk with advises reviews another desk's
work once that desk is done. It runs so it can read but not write, and it
files nothing. Advisors run in a relay, not in a fan.
A desk's kit loads into its runs. MetaBot looks for each skill where Code
Desks does: the office's .office/skills, then the project's .claude/skills
and .agents/skills, then the same two folders in your home. It copies the
skills for the office run and gives them to the engine the way it gives a
plugin, and removes the copies when the office run ends. A kit entry is a
skill's name, never a path. A skill from the project must stay inside the
project, and a skill's hooks and servers are left out. A kit that cannot load
stops the office run before any desk starts.
--checks runs the office's gate checks after each desk that changed a file,
as their own run with their own receipt. A desk whose checks fail stops the
relay. A check runs a command from the office file on your machine, so none
runs until metabot office trust has shown it and you agreed.
MetaBot never pushes, and it never commits to your branch. It commits each
desk's work on the desk's own branch. metabot desks pick stages one desk's
work in your checkout as a squash, and you commit it. metabot receipts show
on an office run prints its tree of desks.
Mail between sessions
Two agent sessions can leave each other notes, whichever harness each one runs in. A mailbox is a name you give a session.
metabot mail send builder --from main -m "the shell suite is yours next"
metabot mail read --for builder # prints the notes and marks them read
metabot mail wait --for main --timeout 900 # waits for a note, prints it, exits
A note is a file in mail/<name>/, in the shape of Code Desks' desk mail, and
it is read once. A harness hook brings a session its mail on its own. For
claude, put metabot mail hook claude <event> --for NAME on its
UserPromptSubmit, PostToolUse and Stop hooks, with the events prompt,
tool and stop. For cursor-agent, put metabot mail hook cursor <event> --for NAME on beforeSubmitPrompt, postToolUse and stop. At a prompt, or
after a tool call, the hook adds the mail to the turn. At stop it hands the
mail back as the next turn, unless you stopped the turn yourself. A hook that
cannot read the mail adds nothing, and the turn goes on.
A note is shown as a note from a session, never as your word. It approves nothing: a write still asks wherever it runs.
Plugins
Plugins carry skills, commands and agents, in the .claude-plugin folder
format. MetaBot pins each one by the hash of its files. This version installs
from folders on this machine.
metabot plugin marketplace add mine ~/my-plugins
metabot plugin marketplace list
metabot plugin install code-desks@metabot # shows what it carries, then installs it, off
metabot plugin enable code-desks@metabot
metabot plugin list
metabot plugin show code-desks@metabot
metabot plugin import-skills --name my-skills ~/my-skills
install lists what a plugin carries before it installs: skills, commands,
agents, hooks and servers. Installing another version of a plugin you have
shows what changes. A plugin's hooks run outside MetaBot's gate, so
metabot plugin enable asks you to allow them or leave them out. Its servers
never start on their own. --add-servers makes them connections, behind the
gate. The metabot marketplace ships with the package and holds the
bizopsmap and code-desks plugins.
Routines and serve
A routine is a goal that runs on a schedule, with no one there to ask.
metabot routine save triage --cron '0 9 * * 1-5' --tz Europe/London \
--connection gh --allow gh__create_issue "Triage the new issues."
metabot routine list # when each is due, and how it last fired
metabot routine run triage # fire it now
metabot routine firings
metabot routine disable triage
metabot serve # fire routines as they come due
The routine uses the gh connection added above, so gh has to be active
when you save it: a revoked connection cannot go in a routine.
A firing is written down before its run starts. It declines every write you
did not allow when you saved the routine, and every destructive call.
--criteria exit_zero judges every step and seals the verdict in the
receipt. metabot serve holds a lock, so one serve runs per home, and it
fires one routine at a time. A due time missed while serve was down is not
fired later. After a restart, a firing that never sealed is marked an error.
The web UI
metabot ui
It prints an address on 127.0.0.1 that carries a token made at start. The
page shows the runs, each run's steps and approvals, an office run's tree of
desks, the connections and the routines. From its chat you start a run and
answer its asks: Allow for a write, and the word yes for a destructive
call. From its office page you start an office run, with the desks and the
mode you pick. Each desk's asks wait on the run's page, and each names its
desk. Its pages hold no script. The address stops working when metabot ui
stops, and every run it started is stopped and sealed.
Files it keeps
Everything lives in ~/.metabot, or where METABOT_HOME points. That folder
is 0700, and so is each folder MetaBot makes for its records. Its settings,
records and tokens are 0600. Desk worktrees, installed plugins and kits are
copies of files, with the modes your umask gives them, inside those 0700
folders.
| Path | What it holds |
|---|---|
config.json |
your settings |
runs/<run>/journal.json, receipts/<run>.json, runs.jsonl |
each run's journal, its signed receipt, and the index |
connections.json, secrets/ |
your connections, and their tokens |
plugins.json, marketplaces.json, plugins/ |
plugins and where they come from |
routines.json, firings.jsonl, serve.lock |
routines and what each firing did |
offices/, desks/, trust/ |
office runs, desk worktrees, and the checks you trusted |
kits/ |
the desks' kits, while their office run goes |
mail/ |
each mailbox's notes, and read/ for the ones read |
Limits
- A connection's server runs with your privileges. MetaBot classes and gates each call the server receives, but the server's own process can do whatever you can.
--allow-writelets an engine run code the classifier cannot see into. A command such as running a script is a write, and once allowed it runs whole, whatever the script does. A destructive call still asks.- Reads never ask. That includes a shell read of a credential file, such as an SSH key or a cloud credentials file. It also includes your signing key, and a key that leaves the machine can sign receipts as you.
- The
codexandcursorengines run their own tools in their own sandbox. MetaBot records those calls after they run, and its gate covers the calls to your connections. A routine runs these engines so they cannot write. Past a run's cap on tool calls, such an engine may already have run the call the run stopped at. - A plugin's hooks, once you allow them, run outside the gate.
- A routine cannot run a project's own test command. A shell command is a write, and a routine's allows name connection tools, so it declines.
- Some local models answer tool calls as text and never act. The roster checks that a local server answers, not that its model can call tools.
- On a
clauderun, a shell step carries no exit code theexit_zerocriterion can read. A failed command is recorded as text, so a run on this engine that declaresexit_zerofails it. - The browser policy cannot see a click that moves the page. A call is
classed by the address it carries, or by where the page was last sent. A
click or a redirect that moves the page to another site is not seen, so
under
--allow-writethe typing after it runs unasked. - Project copies of receipts show as untracked files in your repository.
Commit them or ignore them. MetaBot leaves
.gitignorealone. - Your browser sends the web UI's cookie to every port on 127.0.0.1 while the UI runs. Another local server you open in that time gets it, and could use the UI as you: start a run and answer its asks. The cookie stops working when the UI stops.
Licence
Apache-2.0. See LICENSE.
Metadata
Release files for orcha-metabot 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| orcha_metabot-0.1.0.tar.gz | 326.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| orcha_metabot-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 534.4 kB
Release files / orcha_metabot-0.1.0.tar.gz
| Download URL | orcha_metabot-0.1.0.tar.gz |
|---|---|
| Size | 326.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6ce60dd0b730f0902d8c28175337595ee5e9356b01387ff1e765a29291c56b5f
|
|
BLAKE2b-256 checksum How to use checksums |
fc259817c6035b4d638b142f48cf2e637efae1b07429d0d85a959d3a1ecc5dc2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logRelease files / orcha_metabot-0.1.0-py3-none-any.whl
| Download URL | orcha_metabot-0.1.0-py3-none-any.whl |
|---|---|
| Size | 208.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
01c6277aad31ffb48fef39f0ebb0569f0b4371893515178fc8066efc8f9e7daf
|
|
BLAKE2b-256 checksum How to use checksums |
ec6393296de8bedeb008f48154cb4667e220173d432ebae5d75c1b5aea238466
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency log