Catenary:代码智能命令工具

Catenary 是一款代码智能命令工具,为 AI 编码代理提供基于语言服务器的结构化搜索、导航与诊断,并通过 PreToolUse 钩子强制执行代码智能工作流,以减少上下文消耗并提升 AI 辅助软件开发效率。

  • by TwoWells
  • Rust
  • 本地部署
  • 8 星标
  • 更新于
在 GitHub 查看 Mark-Wells-Dev/Catenary

使用说明

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 PreToolUse hook 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 PreToolUse allowlist 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)? Run cargo uninstall catenary-cli first — or cargo uninstall catenary-mcp if you installed before 2.1.0, when the crate carried that name. ~/.cargo/bin usually precedes brew's bin dir on PATH, 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 doctor after: 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

License

AGPL-3.0-or-later — see LICENSE for details.

Commercial licensing available for proprietary use — see LICENSE-COMMERCIAL. Contact contact@twowells.dev.