使用说明
Catenary
Status: archived (October 2026)
I'm retiring Catenary. The idea worked. The implementation is the part that stopped making sense to maintain.
The idea was that you get better behavior out of a coding agent by construction than by instruction: take the generic tools off the menu, put one code-intelligent surface in front of the agent, and enforce it with a hook. The agent follows the workflow when there's no other path. That held up for me in daily use across every project I run, and the post-mortems on this year's agent incidents, the PocketOS database deletion in April and the Hugging Face breach in July, came to the same conclusion: the controls have to live where the agent can't reach them.
It didn't start out that way. Catenary began as language-server access over MCP: hover, definitions, references and diagnostics as tools an agent could call. Agents didn't reach for them on their own. So I did two things. I shrank the surface: instead of a dozen LSP tools, two commands the agent already had habits for, a grep and an ls, enriched with what the language server knows. And I added deny lists in the clients to steer agents toward them. Then I moved the deny list into Catenary itself, and that's where the hook came from. Once Catenary owned the list it could do fine-grained allowlists, denying some forms of a command while allowing others, which in turn needed a proper bash parser.
The lists kept growing, because the hook wasn't only a gatekeeper. Catenary tracked which files an agent had touched so it could insist on a diagnostics check afterward. That meant inferring intent from commands, telling a perl one-liner that edits a file apart from one that runs arbitrary code. It meant keeping state per agent, which meant giving each agent an identity and carrying it through the hooks over IPC. Each layer made sense when I added it. Taken together it's a lot of code, and somewhere in there I started seeing code as a liability to be maintained rather than an asset.
Catenary was built with Catenary. Every session that wrote this code ran through it, from the first hook on, and I didn't write a line of it by hand. What I did instead was hold the line on the engineering: clippy on pedantic, mutation testing, every regression pinned with a test, and every breakage I hit while dogfooding enshrined in one. And I read the plans instead of the code. The planning repo ended up the same size as the codebase, and since the code was rewritten so often, the plans and the tests were the real source. Git on the code was snapshots.
The maintenance cost came from both sides of the hook. The agent clients Catenary sits in front of move fast. Hook systems, permission models and tool surfaces change month to month, and a third-party layer between an agent and its host has to chase every one of those releases. The original plan had an answer for that, a client of my own, so I'd control both ends. That isn't possible with the subscription plans most individuals use, and individuals are who Catenary was for. On the other side, every language server needed its own exceptions, and Catenary ended up with a blessed list of servers, each with its own known behaviors to work around. So Catenary is a plugin trying to be a client, and I don't see the effort to keep it there as worth my time.
Meanwhile the clients started shipping the important pieces themselves: permission allowlists and language-server integration. Once the platform does what your tool was for, the right move is to let it.
I learned a lot from this one. An agent running as you treats restrictions as obstacles, not guardrails. It will find the path you didn't close. It's also easy to over-restrict, and every rule you add makes the agent a little less useful. If you actually need the agent kept out of something, the answer is a real sandbox, not a smarter deny list. A layer that sits in front of every tool call is on the critical path of every session: when Catenary broke, it failed shut and blocked work. And the limit turned out to be review, not code. Dispatching whole features means one person reviewing a team's output, and the stronger the model, the more arrives finished and built upon before anyone has looked at it. Process organizes that reading. Nothing shrinks it.
What's still going: Lattice, the markdown linter that grew out of this work, is maintained. And the way of working Catenary came from, enforce by construction, instrument everything, retire on evidence, is how I run my other projects. The code stays up as a reference: a daemon that multiplexes many agents onto a shared pool of language servers, a PreToolUse allowlist whose denials point at the sanctioned alternative, resolve-or-deny writes, and an edit-debt gate for diagnostics. The last release, the Homebrew formula and the AUR packages are left as they were. None of it is maintained.
— Mark Wells, October 2026
Catenary hands an AI coding agent a small, opinionated set of
code-intelligent commands — and a hook that keeps it on them. Reach for
grep and you're redirected to catenary grep; reach for ls or find
and you get catenary glob. Every command the agent can run is backed by a
language server, so it navigates code by meaning instead of brute-forcing
text. The generic path isn't blocked for safety — it's off the menu, so
the code-intelligent one is the only path left.
Why Catenary
Exposing language-server tools to an agent isn't novel anymore — most
coding CLIs do it, and Catenary did it early. But having a tool and
using it are different things. Give an agent grep, find, raw file
reads, and LSP navigation, and it reaches for whatever's nearest —
usually brute-force text scanning that burns context and misses structure.
Catenary takes the choice away. It exposes one curated, code-intelligent surface and enforces it:
- A
PreToolUsehook runs an allowlist over every shell command the agent issues. - Denied commands aren't dead ends — each denial names the
code-intelligent command to run instead (
grep→catenary grep,ls/find→catenary glob). - Edits flow through the host's tracked Edit/Write tools, so LSP
diagnostics come back automatically when the agent runs
catenary diagnostics.
The result is a workflow the agent follows by construction, not by prompt-engineering.
This is enforcement of a workflow, not a security sandbox. The hook is a cooperative contract — a Makefile can still run anything, and Catenary doesn't isolate the filesystem or environment. Its job is narrower and more useful: keep the agent's reads on code-intelligent search and its writes on a tracked path, so navigation is structural and diagnostics are free.
Less context, more signal
A grep-and-read loop pulls whole files into the agent's context window and
re-processes them every turn. catenary grep answers with the symbol, its
signature, and where it's used — tens of tokens instead of thousands.
Diagnostics arrive through catenary diagnostics stdout, so the agent
never re-reads a file just to check whether its edit compiled.
The surface
Search — always available, no setup beyond installation:
| Command | What it does |
|---|---|
catenary grep <pattern> |
Symbol, reference, and text search — LSP-enriched within tracked roots |
catenary glob <path> |
File outlines, directory listings, glob matches |
The edit → diagnostics loop — run in the host's shell tool:
# Edit files with the host's native Edit/Write tools. Editing starts
# automatically on the first change to a server-covered file — there is
# no start step.
catenary diagnostics # print LSP diagnostics for every file you
# touched, then clear the set
catenary diagnostics is the end of an edit batch: it opens the
modified files on their servers, waits for each to settle, and prints the
errors and warnings — like a linter, it's silent on success. For sweeps
too broad for per-file edits, reach for native sed -i — the hook
resolves its write-set and folds the changed files into the same
diagnostics batch.
Workspace roots — manage which directories are indexed:
catenary roots add <path> # index a directory
catenary roots rm <path>
catenary roots ls
How it fits together
One daemon per host manages a shared pool of language servers. Multiple
agents connect over a Unix socket and share those servers — a single
rust-analyzer serves every session on the same project. Catenary reaches
the agent through four decoupled surfaces; none depends on the others.
agents ──▶ Catenary daemon ──▶ shared LSP server pool
rust-analyzer · pyright · gopls · …
reached through four decoupled surfaces:
CLI grep · glob · diagnostics — via the host's shell tool
Hooks allowlist enforcement — one PreToolUse hook
MCP heartbeat + workspace roots — no query tools
TUI live observability — protocol & trace traffic
- CLI — the code-intelligent commands above, invoked through the host's shell tool. Stateless: each command connects to the daemon, delegates to the right language servers, and prints to stdout.
- Hooks — the
PreToolUseallowlist that enforces the workflow and tracks edited files for the diagnostics batch. - MCP — a heartbeat only: the protocol handshake, the workspace-roots channel, and user-facing notifications. It advertises no query tools.
- TUI — real-time observability across every session and language server.
Quick Start
1. Install
Homebrew (macOS and Linux):
brew install twowells/tap/catenary
Switching from a
cargo install(the previously recommended path)? Runcargo uninstall catenary-clifirst — orcargo uninstall catenary-mcpif you installed before 2.1.0, when the crate carried that name.~/.cargo/binusually precedes brew's bin dir onPATH, so the stale binary keeps answering otherwise.
Prebuilt binary (Linux x86_64 / macOS arm64):
curl -fsSL https://raw.githubusercontent.com/TwoWells/Catenary/main/install.sh | sh
From source (any platform with a Rust toolchain):
cargo install catenary-cli
The catenary binary must be on your PATH before configuring any
client. Plugins and extensions provide hooks and the MCP declaration but
do not include the binary — this step is required.
Upgrading from 1.x? Every breaking change in 2.0 is to user configuration — read the migration guide before upgrading, and run
catenary doctorafter: it flags each stale config form with the exact rename.
2. Configure language servers
Add your language servers to ~/.config/catenary/config.toml:
# The [lsp.server.*] section key IS the binary Catenary spawns; bind a
# language to it under [lsp.language.*]. Add `path = "/abs/path"` only to
# relocate a binary that is not on PATH.
[lsp.server.rust-analyzer]
[lsp.server.pyright-langserver]
args = ["--stdio"]
[lsp.language.rust]
servers = ["rust-analyzer"]
[lsp.language.python]
servers = ["pyright-langserver"]
Catenary can install the servers itself. Opt in with:
[servers]
auto_install = true
Any configured server that has passed Catenary's conformance gate is then
installed at its vetted, pinned version into a Catenary-owned directory —
in the background, at session start, with no per-server install step.
Servers already on PATH are left alone. The opt-in is honored from your
user config only; a project .catenary.toml can never switch it on.
(catenary install is unrelated: it installs host plugins, not language
servers.)
3. Connect your agent
Claude Code
claude plugin marketplace add TwoWells/Catenary
claude plugin install catenary@catenary
Antigravity CLI — copy plugins/catenary-antigravity/ to
.agents/plugins/catenary/ in your workspace.
4. Verify
catenary doctor
doctor reports each configured server's status (ready, command not found, spawn failed, initialize failed) and whether the host's hooks
are installed and current. Managed installs count as installed, and a
system-installed server whose version drifts from the vetted pin draws an
advisory finding naming both versions. Pass a server name (catenary doctor rust-analyzer) for verbose single-server diagnostics.
Observability
Run catenary in a terminal to launch the TUI dashboard — a live view of
every session, every language server, and the protocol traffic between
them. Catenary keeps a state.json snapshot of live state and streams
full protocol and trace detail to a sharded JSONL telemetry firehose; the
TUI reads the snapshot, and catenary query reads the firehose. (There is
no SQLite database — a legacy one is drained on startup.)
| Command | Description |
|---|---|
catenary |
Launch the TUI dashboard |
catenary query |
Query the telemetry firehose (by session, server, tool, time, …) |
catenary doctor |
Verify language servers and hook installation |
catenary version |
Show the CLI and running-daemon versions |
catenary stop |
Stop the running daemon |
Documentation
Full documentation at twowells.github.io/Catenary
- Installation — setup for Claude Code and Antigravity CLI
- Configuration — language servers, routing, command allowlist
- CLI & Dashboard — the command surface and TUI dashboard
License
AGPL-3.0-or-later — see LICENSE for details.
Commercial licensing available for proprietary use — see
LICENSE-COMMERCIAL. Contact contact@twowells.dev.
