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.
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.
| 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 |
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 terminalThere 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.
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 rewriteThree 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 readdrift 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.
| 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.
- 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.
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.
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 hereexport 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.
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.
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.
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.
"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 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.
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)
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.
- 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.
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.
MIT or Apache-2.0, at your option. See LICENSE-MIT and
LICENSE-APACHE.

