Skip to content

Repository files navigation

Aviary

A governance console for the configuration of AI coding agents.

Aviary inventories what is already installed across every coding agent on your machine — hooks, MCP servers, skills, plugins, commands, subagents, model bindings, credentials, permissions, rules — audits it, compares it against what the agents actually did, and lets you change it carefully.

It manages what exists; it does not install anything new. Nothing leaves the machine, and there is no telemetry. Every write is backed up, atomic, journalled and undoable.

46 agents have a profile, 21 of them checked against a real installation — the full table is docs/support.md. Adding another is a TOML file, not Rust.

The Fleet view: which installing tool has written into which agent

More of them in docs/screenshots.md. Every one is rendered from crates/aviary-tauri/ui/fixtures/snapshot.json — one machine's real configuration, laid out synthetically and with every name the mapping table knows replaced — rather than captured from anybody's laptop. That is not modesty about the numbers: a picture of a real installation carries account names and paths past every check in this repository, because none of them can read a PNG. scripts/screenshots.sh regenerates them.


Requirements

Rust 1.82 or later; cargo is the whole build
OS macOS for the desktop app (it links Tauri v2). The CLI and the six library crates build on Linux
Node only to run the two interface checks. Never to build or run the app
Python 3 only for the test-corpus script

Install

git clone https://github.com/css521/aviary.git
cd aviary
cargo run -p aviary-tauri          # the desktop app
cargo run -p aviary-cli -- audit   # or the same findings on a terminal

There is no configuration step. Aviary looks for the files its profiles declare, reads what is there, and reports what it could not read rather than failing.

Building a .dmg is two commands — see docs/packaging.md. The bundle is unsigned, so the first open needs right-click → Open.

Quick start

Everything here is read-only:

cargo run -p aviary-cli -- audit          # findings, worst consequence first, and any recommendation
cargo run -p aviary-cli -- inventory      # per-agent counts of what is installed
cargo run -p aviary-cli -- fleet          # which installing tool reached which agents
cargo run -p aviary-cli -- instructions   # which prose each agent loads, and whose repository it is from
cargo run -p aviary-cli -- history        # conversations
cargo run -p aviary-cli -- search <query> # across every conversation
cargo run -p aviary-cli -- discover       # agents on disk that no profile covers
cargo run -p aviary-cli -- profiles       # loaded profiles, and any that failed
cargo run -p aviary-cli -- roundtrip ~/.claude/settings.json   # prove a file survives a rewrite

Three commands write, and only ever to ~/.aviary — never to an agent's own configuration:

cargo run -p aviary-cli -- drift              # what changed since the last scan
cargo run -p aviary-cli -- drift --record     # ...and advance the baseline (for cron)
cargo run -p aviary-cli -- usage --summarise  # roll up usage for the audit to read

drift without --record deliberately does not advance the baseline: consuming the window from a terminal would mean whoever opens the app next is told nothing changed. Both exit non-zero when something moved, so either can go into CI.

Configuration is six surfaces, not just hooks

Surface Entities
Model access provider, model binding, base_url, auth method, credential
Behaviour injection hooks
Capability extension MCP servers, plugins, skills, commands, subagents
Constraints permissions, rules (AGENTS.md, .cursorrules, bespoke DSLs)
Environment injected env vars, statusline
Provenance & drift who installed each entry, when, and whether it changed since

Model access matters most, because it has the highest cross-agent duplication and the highest cost of error: one gateway URL written separately into two agents is one you can change in one place and miss in the other, and the agent that kept the old endpoint says nothing about it anywhere.

The app: six views

The Audit view: findings ordered by what the consequence costs

  • Audit — the landing page, because it is the only opinionated output here. One sentence naming the single costliest finding, then findings ordered by what the consequence costs, folded by kind. Each can be marked "this is intentional", which holds until the entry it points at changes — the identity is derived from the entry's content hash, so an edited hook comes back to be asked about again.
  • Fleet — how far each installing tool has spread, ranked by agents reached, with the owner × agent matrix behind it. Acting on a whole row is one action.
  • Inventory — agent/kind tree, filterable entity list, detail pane showing location, provenance with its evidence, whether a native disable flag exists, and any credentials (never their values).
  • Skills & Plugins — every skill any agent can load, grouped by the plugin that brought it, with what each one adds to the front of every conversation.
  • Instructions — every AGENTS.md-style file an agent reads, largest first, each naming the agents that read it and the repository owner it came from, split into the ones cloned from elsewhere and the rest. Per-agent totals, because no agent reads all of them and the amounts are nothing like each other. Any row opens the text itself. Where no conversation roll-up has been recorded the page says nobody has looked, which is not the same answer as "none".
  • History — what the agents actually did: conversations, tokens, tools, projects, a search across all of it, an activity calendar, and what it would have cost.

The interface is fully localised (English, Simplified Chinese). Findings are translation keys plus parameters, never English sentences assembled in Rust.

Writes

Enable/disable (a native flag where the format has one), field edits, delete, export/import/deploy/undeploy/remove for skills and plugins, and one action per installing tool — because a report that says "removing this means editing four files" and then asks for a separate confirmation per entry has named work it declines to do.

Every write is preceded by a preview of the bytes it will change, produced by the same code as the write itself, and by a re-scan: these files have many writers besides you, so a write based on the snapshot the window was showing would silently discard one of their changes. The digest checked is seconds old, and a file that moved under us is refused by name.

The audit makes exactly one recommendation — a shorter hook timeout — and the number is the median of the plausible timeouts on your own machine rather than one this program invented. Where fewer than five hooks declare anything plausible, it offers nothing.

Off the machine: an artefact, a comparison, and a gate

Everything above is a view of one laptop, which cannot answer three questions people have anyway: what differs between my machine and my colleague's, what has moved since the release we reviewed, and does this pass. Those need the configuration as a file.

cargo run -p aviary-cli -- export > agents.json            # the whole configuration as a file
cargo run -p aviary-cli -- diff reviewed.json agents.json  # what moved since that review
cargo run -p aviary-cli -- audit --json                    # the same findings, for something other than a person
cargo run -p aviary-cli -- check --policy policy.toml      # non-zero on what is not allowed here

export holds modelled fields, digests, home-relative paths and credential presence — storage mode, fingerprint, length, encodings — and no entry contents, for the same reason the drift baseline holds none: the raw entry is where the secrets are. Two exports of an unchanged machine are byte-identical, because the run's timestamp sits outside the comparable body; without that property git diff would report a change every time anybody ran it and the file could not be reviewed. The same configuration under two different usernames also exports identically — entries are keyed partly by path, so keying on the absolute path would have made every entry on one machine a different entry from its twin on another, and a cross-machine diff would report the whole configuration as replaced.

diff is the drift comparison over two files instead of two scans — the same definition of "the same entry", rather than a second one that agrees until it does not.

check --policy judges the machine against something declared; see policy.example.toml. Findings that are unacceptable here (by rule id, or by capability for the rule that reports seven of them), ceilings on severities, on hooks that run per tool call, on hook timeouts, on entries nobody can be attributed for, plus allow/deny lists for installing tools and MCP servers.

Two things it does on purpose. Every clause is expressed in the audit's own terms rather than in a query language, because a policy that could ask its own questions would be a second analysis engine to keep in agreement with the one that has the measurements behind it. And a clause that cannot be judged is reported as unevaluated rather than passed: two rules need the usage roll-up, and a gate that passes because nothing was measured is the failure mode of gates. Acknowledgements are not consulted either — "I know about this" is one person on one laptop, a policy is a place saying "not here", and letting the first silence the second would make the gate advisory.

What it can say that a single agent cannot

Three of these exist only because the configuration side and the history side are read by the same program:

  • What a hook has cost. Hooks per call × that agent's own recorded tool calls. Multiplied per agent, never fleet-wide: one agent's hook count and another agent's call count have nothing to do with each other, and a fleet-wide product would be a large number that means nothing.
  • Whether a matcher has ever matched. A hook waiting on a tool name that never appears in the agent's history is either dead or watching for something that never comes, and both are worth knowing. Only plain name lists are judged — a matcher with regex metacharacters is left alone, because an approximation here eventually tells someone a live hook is dead.
  • Which agents cannot be measured at all, and why. Four different reasons, said out loud, because silence reads as "nothing to report": history not read, history empty, a format with nowhere to put a tool call, or per-call events nobody has established.

And one surface nothing else inventories at all: the instruction files. An AGENTS.md, CLAUDE.md or .cursorrules is read whole at the start of a conversation and followed, which makes it configuration written in English — and the only kind here that arrives from outside without being installed. Clone a repository, open it, and its prose is now instructions to something that can read your files and run commands.

Two things make that answerable. Which agent reads which of those files is a property of the agent, so it is declared per agent, per scope, with verified saying whether anybody checked it against a real installation. And where to look comes from the conversation history: the projects your agents have actually worked in, rather than a crawl of the disk guessing what a project is. Whose repository each file is in is read out of .git/config — no git process, no library, since the question is one line of an INI file. The owner segment of the remote URL is reported and nothing more is claimed: deciding which of those owners is you is a judgement, and a tool that made it would eventually call your own repository foreign, or the reverse.

And one that needs neither side: drift. Comparing a scan against the stored baseline answers "what changed while I was not looking", which on a machine where every one of these agents rewrites its own configuration has no other source. The log is append-only, so yesterday's change is still readable after ten scans today. Entry contents are never stored — keeping them would mean copying every credential on the machine into ~/.aviary in order to notice one had changed — so a change is reported as timeout 5s -> 86400s where the field cannot be a secret, and as "changed" where it could.

Rules this project holds itself to

Credentials are never displayed in plaintext. Not truncated, not masked — the value never leaves aviary-core::secret. You get storage mode (inline / env reference / command reference), a fingerprint, and locations. A tool that renders twenty agents' keys on one screen is a better credential harvester than most malware. Fingerprints normalise scheme prefixes and one layer of base64/hex, so the same secret correlates across agents and encodings without the value being held.

Credential files are never opened. auth.json, oauth_creds.json, *token*.json, google_accounts.json are on a deny-list — not redacted, never read.

Numbers are read, never estimated. Token counts come out of the records or are reported as absent; and 0 are different claims and the interface distinguishes them. The one derived figure is money, and it is derived under a stated rule: a conversation that used two models at different rates is left unpriced and counted, because tokens are recorded per conversation and splitting them would be arithmetic nobody measured. Several models at the same rate are priced exactly, since the total does not depend on the split. Rates carry the date they were read and the page they came from.

A rule that cries wolf gets the whole report ignored. So findings state a consequence and stop; they do not tell you your setup is wrong. Hooks that fire on every tool call may be exactly what someone wants from an observability tool — the value is in knowing that eight processes launch per call.

Text formats are read-only where they cannot be edited losslessly. Markdown and bespoke DSLs are toggled by moving the file rather than rewriting it; mutation returns an error instead of silently reformatting. Bespoke rule DSLs are reported as existing and shown verbatim, not parsed — a half-working parser would confidently mis-report what a rule permits.

Agent support is data, not code

Hand-writing an adapter per agent does not scale to 46 profiles. It is also unnecessary: eight structural shapes cover all of them.

Shape Profiles declaring it
dir-of-files 42
named-map 17
scalar 16
hooks-by-event-groups 14
value-map 12
hooks-by-event-entries 3
hooks-array (TOML) 2
entry-array 1

Event names differ per agent (PreToolUse / BeforeAgent / afterAgentResponse / preToolUse), and so do field names — one agent spells its command field bash and its timeout timeoutSec, another counts timeouts in milliseconds. All of that is profile data, including the unit, because an earlier version guessed the unit from magnitude and reported a 5,000 ms timeout as an 83-minute hang.

Profiles load at runtime from three layers, later overriding earlier by id:

crates/aviary-profile/profiles/*.toml   built-in
~/.aviary/profiles/*.toml               user
<cwd>/.aviary/profiles/*.toml           project

There is a test asserting the shape interpreters contain no agent-specific branches, because an extension point that needs a recompile is not one.

Writing that file does not mean reading someone else's format

"Profiles are data" is only half an extension point — the other half is that somebody still has to work out what the data is. So aviary discover infers it, and the way to see whether that works is to take the profiles away:

$ AVIARY_PROFILES_DIR=/tmp/empty cargo run -p aviary-cli -- discover
AGENTS WITH NO PROFILE (19):

suggested id         entries  inferred
claude                    72  72 hook @/hooks (high)
cursor                    40  38 hook @/hooks (high), 2 mcp-server @/mcpServers (high)
codex                     33  5 mcp-server @/mcp_servers (high), 9 skill @/skills/config (high), …
gemini                    22  18 hook @/hooks (medium), 4 hook @/flowguard-status (low)
…
Draft one with:  aviary discover --draft <id> > ~/.aviary/profiles/<id>.toml

Every agent that has a profile comes back without one, hook location and all, from nothing but the shape of the file. Detection is purely structural — a container whose values are arrays of objects carrying a command-like string is a hook table, whatever it is called — which is what makes it work on agents that did not exist when the code was written.

The (low) is the part worth trusting: a vendor's own key in one agent's config looks a little like a hook table, and the guess is made and labelled rather than suppressed. Drafts are written verified = false and every inference carries its confidence, because the failure to avoid is not guessing wrong — it is guessing wrong and presenting it as fact. A test asserts that a generated draft parses, validates and loads, since a draft the user cannot save is not an extension point either.

Conversation history is declarative in the same way: 33 provider descriptors covering JSONL, JSON, Markdown-per-directory, protobuf and SQLite. A SQLite-backed agent declares its queries in its own descriptor rather than getting a Rust function, because eleven schemas would otherwise be eleven functions — and SQL is already declarative.

The UI has no build step

The interface is plain HTML, CSS and 28 ES modules under crates/aviary-tauri/ui. cargo run -p aviary-tauri is the whole toolchain — no npm install, no bundler. For a tree, some lists and a few charts a framework would add a build pipeline without adding capability, and one fewer dependency chain matters more for a tool that reads security-relevant files.

Nothing inspects that JavaScript before the webview does, and the failure mode is silent: a reference to a function that moved raises a ReferenceError inside a click handler, and what the user sees is a panel that renders nothing. So node crates/aviary-tauri/ui/check.mjs checks what a compiler would — every import names a real export, every cross-module reference is imported, no name is declared twice, every view name has a template, every literal translation key exists — and then links the whole graph in Node.

That leaves what the words say once they are on screen, which is where this interface's defects have actually been: 1 files will change, a translation key rendered as itself, a panel drawing nothing because the data arrived in a shape it did not expect. node crates/aviary-tauri/ui/render.mjs renders all seven views against a committed fixture — one complete snapshot, scanned from a synthetic machine built out of the test corpus rather than from anybody's laptop — and reads the text back. It knows nothing about CSS, so everything needing a layout still needs eyes.

Repository layout

crates/
├── aviary-fmt/       lossless engine: JSON/JSONC, TOML, Markdown, opaque text
├── aviary-profile/   declarative profiles + the shape interpreters
├── aviary-core/      normalised model, provenance, credentials, safe-write store, drift
├── aviary-audit/     security and governance rules
├── aviary-history/   conversation history: providers, sessions, usage, search, cost
├── aviary-cli/       verification and debugging entry point
└── aviary-tauri/     desktop app (Tauri v2; UI is plain ES modules, no build step)

Contributing

The most useful contribution is a TOML file, not Rust. docs/adding-an-agent.md walks it end to end, and docs/support.md shows where the blanks are — the two behavioural columns are answered for one agent out of forty-six, and each answer is a line.

Conventions, the five gates, the test corpus, and why cargo fmt is deliberately not run are in CONTRIBUTING.md.

Roadmap

  • M0 lossless engine — done
  • M1 profiles, normalised model, provenance, audit rules, the cross-agent fleet view — done
  • M2 in-place enable/disable and editing: native flags where the format has one, shadow store only where "disabled but retained" cannot be expressed; backup, atomic write, journal, undo — done
  • M3 conversation history: 33 providers including SQLite, usage, search, cost — done
  • M4 drift as a first-class view: currently a scan-against-baseline comparison with an append-only log. A filesystem watch would make it live, and the log deserves a calendar rather than a list.
  • Wanted next: more agents (a TOML file each), and the three fidelity gaps recorded in the provider descriptors — tool results paired across records, usage keyed by message id, session-level usage in a shutdown event.

Non-goals

Not an installer, skill marketplace, or plugin store. Not a multi-agent run orchestrator — it does not manage sessions or execute agents. It does not change models or provide completions. Nothing is uploaded anywhere, and there is no telemetry.

Licence

MIT or Apache-2.0, at your option. See LICENSE-MIT and LICENSE-APACHE.

About

A governance console for the configuration of AI coding agents: inventory, audit and carefully change what every coding agent on your machine already has installed.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages