Skip to main content

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-write lets 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 codex and cursor engines 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 claude run, a shell step carries no exit code the exit_zero criterion can read. A failed command is recorded as text, so a run on this engine that declares exit_zero fails 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-write the typing after it runs unasked.
  • Project copies of receipts show as untracked files in your repository. Commit them or ignore them. MetaBot leaves .gitignore alone.
  • 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)

Source distribution for orcha-metabot 0.1.0
File Size Uploaded
orcha_metabot-0.1.0.tar.gz 326.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for orcha-metabot 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page