Skip to main content

planeops: a control plane for a personal AI setup

CI Coverage OpenSSF Scorecard PyPI Python versions Platforms: macOS and Linux License: Apache-2.0

Coverage · Install · Quickstart · Commands · Docs · Spec

Write down your AI setup once.
Find out the moment reality disagrees.

planeops keeps an inventory of the AI tooling on your machine and tells you when it stops matching what you meant: the coding assistants and their hooks, MCP servers and which clients they are wired into, local models, background services, packages, config, and API keys. You declare what should exist and why in plain YAML. One read-only scan compares that to the machine. Nothing is changed until you have seen the diff and said yes.

$ plane drift
✗ 5 alert(s) on mymac 09:14

alerts (5)
  launchd
  ✗ ai.gateway           expected present, not observed
  ✗ com.example.updater  ungoverned always-on service; declare it or name it exactly in unmanaged
  ollama
  ✗ qwen3:8b   expected present, not observed
  ✗ legacy-7b  listed retired but still observed present
  secrets · required secret is not configured
    ✗ router-api-key

  full report ~/planeops/observed/mymac/DRIFT.md

One scan, five alerts, four kinds of problem: a service that stopped, a service that installed itself and runs at login, a model a cleanup pruned, a model you retired that is still taking disk, and a key nothing ever configured.

Why

Your machine accretes AI tooling. A coding assistant here, its hooks running shell on every tool call. MCP servers wired into one client and forgotten in the other three. Local models from an experiment you finished months ago. Background agents that installed themselves at login. Keys in dotfiles, in .env files, in your shell profile.

None of it is written down. There is no record of what is installed, how it is wired, or why you put it there. So the questions that matter have no answer: what is running right now that I did not choose? What broke when I reinstalled? What can I safely delete?

An ungoverned pile of tools on the left; the same tools declared and connected on the right

planeops turns the pile into a registry. Every asset is declared with the reason it exists, every scan is diffed against that intent, and every fix is a rendered change you confirm one at a time.

Highlights

  • One scan, thirteen adapters. Services, models, four package managers, MCP clients, assistant hooks, secret stores, and the config traces tools leave behind, in a single read-only pass.
  • Observation never writes. No daemon, no background process, no open port. plane observe reads the machine and writes one snapshot file.
  • Nothing mutates behind your back. plane apply renders each change as a diff and asks, one entry at a time.
  • It knows how to be quiet. A dormant project is parked, so silence is correct. A finished retirement asks you to tidy the registry instead of alerting forever. Per-entry tolerance decides what is worth waking you for.
  • Every MCP server across every client, in one view. Claude Code, Claude Desktop, Cursor, and Codex, merged, so a server wired into one and missing from the rest is visible.
  • Secrets stay sealed. Names and presence are tracked in the open, values are encrypted at rest with sops and age and never enter a snapshot or a report.
  • Ambient without a daemon. plane schedule hands the loop to launchd or systemd, and plane status --short puts the result in your shell prompt.
  • Scriptable by design. Exit 0 clean, 1 operator error, 2 drift, with --json on the reporting commands.
  • Names no vendor. Adapters, platforms, schedulers, secret stores, MCP clients, and assistant harnesses are all discovery seams. Adding one is a new file, not a patch to the engine.

What it sees

Thirteen adapters ship today, each observing a domain the others do not. The ones that need configuration (mcp sources, footprint roots, the secrets store) do nothing at all until you set them up.

Adapter What it tracks
launchd macOS user agents: loaded, running, and whether they start themselves at login
systemd Linux user units: enabled, active, and the same login question
mcp MCP servers merged across Claude Code, Claude Desktop, Cursor, and Codex, with the scope each wiring uses
harness Hooks your coding assistant runs on its own events, including whether the script they point at still exists
ollama Local models, by name and digest
pkg-brew Homebrew formulae, with versions
pkg-npm npm globals
pkg-uv uv tools
pkg-nvm Node versions installed under nvm
chezmoi Config files reproduced from a chezmoi source, and whether they have drifted from it
footprint Tools discovered by the config directories they leave behind, which is how things nothing else tracks get found
secrets Which declared secrets exist in the store, by name and presence only
manual Anything without an adapter yet, held by a dated attestation that goes stale after 30 days

An entry whose adapter does not exist yet is reported as uncovered rather than as a violation, so you can declare a thing before planeops can see it.

Install

$ uv tool install planeops        # or: pipx install planeops / pip install planeops

Installs the plane command. Python 3.12+, macOS and Linux.

To let your AI assistant query the plane over MCP, install the extra: uv tool install "planeops[mcp]", which adds plane-mcp.

From source (development)
$ git clone https://github.com/albertorsesc/planeops
$ cd planeops
$ uv sync            # engine + plane CLI
$ make check         # the full gate: lint, format, types, tests

Quickstart

# scaffold an instance and seed the registry from what is already installed
$ plane init --seed
create the instance at /Users/you/planeops? (path or Enter to accept)
instance ready at /Users/you/planeops
  wrote 73 entries to /Users/you/planeops/registry/imported.yaml; prune, then `plane drift`

# scan the machine, then diff it against the registry
$ plane observe
✓ observed 73 facts on mymac /Users/you/planeops/observed/mymac/snapshot.json
  pkg-brew 21 · mcp 14 · ollama 9 · pkg-npm 8 · launchd 6 · ...
$ plane drift
✓ no drift on mymac 16:04

# hand the loop to your OS timer, previewed and confirmed
$ plane schedule --every 6h

# one glance, forever after (prints nothing when clean)
$ plane status --short
drift:3

plane init --seed drafts the registry from what you already have, so the first pass is pruning rather than authoring. From there, plane apply walks the drift one confirmed change at a time.

The commands

Command Does
plane init Scaffold an instance, optionally seeding the registry from the machine (--seed). On an existing instance, --sections names the instance.yaml sections you have not adopted yet
plane observe Scan the machine and write a snapshot. Read-only. --attest refreshes manual attestations
plane drift Diff desired against observed, write DRIFT.md and DRIFT.json. --json for piping
plane status Show the last report without rescanning. --short for a shell prompt
plane apply Converge confirmed changes one at a time. Narrow with --id or --phase
plane reconcile Observe then drift in one pass, which is what a scheduler runs
plane schedule Set up the ambient timer (--every 6h, --no-login, --off)
plane secrets init, add, list, remove against the configured store
plane import Propose entries from a manifest, an .env file, or the last snapshot. --write lands them
plane mcp The cross-client MCP view. mcp init detects your clients and wires them as sources

How it stays quiet

An inventory that alerts on everything gets ignored. Two per-entry knobs decide what reaches you, so a quiet report means the machine is genuinely fine.

Lifecycle says what you intend for a thing right now:

Lifecycle Means
active In use. Missing is an alert
maintain Kept working, not actively used. Missing is an alert
parked Dormant on purpose. Absence is correct, so silence is correct
retired Should be gone. Still observed is an alert, and once it is gone planeops asks you to delete the entry
purge Same, and apply may also delete the artifact itself: the plist, the unit file, the client's MCP block

Tolerance says how loudly a divergence lands: auto folds it, report mentions it, alert wakes you. A version bump inside a major can fold while a dead heartbeat alerts.

Read-only by default, gated when it writes

An eye for read-only observation; a locked gate for confirmed mutation

observe, drift, status, and mcp only read. The one command that changes your machine is apply, and it renders each change as a diff and waits for your confirmation before every single one. Adapters shell out to the tools you already trust, so brew, systemctl, and ollama do the work; planeops decides whether, they decide how.

A typo in the registry fails at load rather than quietly meaning nothing, which is the same reason an unknown key is rejected instead of ignored.

Secrets

A safe holding a key, its value redacted

Declare a secret like any other entry and planeops tracks whether it is configured, never what it holds. Values are encrypted at rest with sops and age, and at apply time they are written only into the file the declaring entry names.

$ plane secrets add router-api-key      # prompted, or piped
$ plane secrets list                    # names only
router-api-key
gateway-token

A value never enters a snapshot, a report, or a log line. Details in docs/secrets.md.

Your instance

plane init creates an instance: a directory that belongs to you, not to the tool. Git it like a dotfiles repo.

~/planeops/
├── registry/           desired state: the YAML you declare, edit, and prune
├── instance.yaml       this machine's adapter settings
├── secrets.sops.yaml   encrypted values, if you use the secrets store
└── observed/<host>/    generated per machine: snapshot.json, DRIFT.md

registry/ and instance.yaml are your setup's documentation. observed/ is regenerated by every scan, and one registry can serve several machines, each with its own snapshot. Layout, multi-machine use, and the tool's exact footprint: docs/instance.md.

Your assistant can read the plane

With the [mcp] extra, plane-mcp serves five tools over stdio: rescan the machine, get the drift report, get the last report without rescanning, get the cross-client MCP view, and list secret names. Your assistant answers "what changed on this machine?" from the real snapshot instead of guessing.

There are deliberately no mutation tools. apply stays behind the CLI's per-change confirmation, so an assistant can read the plane and never converge it unattended.

Going further

  • SPEC.md: architecture, entry schema, adapter contracts, exit codes.
  • docs/instance.md: your instance directory, several machines on one registry, the tool's footprint.
  • docs/secrets.md: the secrets flow end to end and how values stay sealed.
  • docs/mcp.md: the cross-client view, the opt-in unwire of retired servers, and the read-only server your assistant queries.
  • docs/footprint.md: discovering tools by their config traces, and keeping debris out of the way.
  • CONTRIBUTING.md: dev setup, the quality gate, and how to write an adapter.
  • CHANGELOG.md: every release and what changed.

Status

Pre-1.0, and it governs this project's own machines daily. The loop, thirteen adapters, scheduling, secrets, importers, and the MCP server all work on macOS and Linux, with both covered by CI on every change. Contracts may still move; a breaking change bumps the minor and lands in the CHANGELOG with its migration.

Built on

planeops delegates instead of reinventing. sops and age hold the secrets, chezmoi reproduces config files, your OS's own scheduler runs the ambient loop, and the package managers you already use keep doing the installing. The engine rides on ruamel.yaml for the registry, Rich with rich-argparse for the console, and the MCP Python SDK for the optional server, and is built with uv, ruff, mypy, and pytest. Thanks to all of them.

Contributing, security, license

Contributions are welcome: fork, branch, and open a PR. CONTRIBUTING.md has the dev setup, the quality gate, and a walkthrough of writing an adapter. Security posture and reporting: SECURITY.md. Licensed Apache-2.0.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

planeops-0.11.2.tar.gz (299.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

planeops-0.11.2-py3-none-any.whl (153.3 kB view details)

Uploaded Python 3

File details

Details for the file planeops-0.11.2.tar.gz.

File metadata

  • Download URL: planeops-0.11.2.tar.gz
  • Upload date:
  • Size: 299.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for planeops-0.11.2.tar.gz
Algorithm Hash digest
SHA256 8af0aa705ce235f0562a781ad17ba44c41ecc46913953ebbb02cc35e69ed11a1
MD5 340d6cd0b3754053bdbe0ed91d41ac25
BLAKE2b-256 6bf88ecfbe019096d942859b8a558c7eea0de8b670dbedd91b241a34e542c353

See more details on using hashes here.

Provenance

The following attestation bundles were made for planeops-0.11.2.tar.gz:

Publisher: release.yml on albertorsesc/planeops

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file planeops-0.11.2-py3-none-any.whl.

File metadata

  • Download URL: planeops-0.11.2-py3-none-any.whl
  • Upload date:
  • Size: 153.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for planeops-0.11.2-py3-none-any.whl
Algorithm Hash digest
SHA256 760db1ecbee32fbc712845979e88f6e725723227db61596e5897cab964957a9a
MD5 759fa6e6d0ab285267aadb6f2a36bacc
BLAKE2b-256 f9ecf7a936522d8d34611ebb0283197a8454d0c340474ba72d06bda6bfbc9aa1

See more details on using hashes here.

Provenance

The following attestation bundles were made for planeops-0.11.2-py3-none-any.whl:

Publisher: release.yml on albertorsesc/planeops

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page