Skip to content

Repository files navigation

cc-context

Give your agent bounded context. Guard hooks catch cat, raw grep, and full-file reads, rewriting the mappable ones into budgeted ccx calls — line-numbered, token-capped, never silently truncated — and windowing or blocking the rest.

CI Release License PolyForm-Noncommercial-1.0.0

Get started

/plugin marketplace add yasyf/captain-hook
/plugin marketplace add yasyf/cc-context
/plugin install cc-context@cc-context

The captain-hook marketplace comes first: the plugin declares captain-hook as a dependency, and it auto-installs only when its marketplace is already known. Upgrading an existing install? claude plugin update silently skips newly added dependencies; add the captain-hook marketplace, then re-run /plugin install cc-context@cc-context.

Terminal running 'ccx code outline internal/astgrep/run.go --budget 60' — a syntax-highlighted file outline cut at the budget, ending in '+5 lines, ~111 tokens omitted'

The plugin arrives wired together. The ccx binary self-provisions, the MCP server auto-registers its mcp__cc-context__ccx_* tools plus BashFormat, the guard hooks turn on, the ccx skill teaches the reach-for-ccx-first workflow, and five read-only reader agents (web-fetch, web-researcher, ci-triage, dep-reader, enumerator) stand by to read pages, logs, and dependency source in their own context and hand back only cited conclusions. It needs uv on PATH, since the hooks run through uvx capt-hook.

Driving with an agent? Paste this:

/plugin marketplace add yasyf/captain-hook
/plugin marketplace add yasyf/cc-context
/plugin install cc-context@cc-context
Using ccx outside Claude Code? Install the standalone CLI

The Homebrew formula pulls in ast-grep and uv:

brew install yasyf/tap/ccx

Release-tarball and go install installs need ast-grep ≥ 0.44 on PATH for the structural ops; install it with brew install ast-grep or uv tool install ast-grep-cli.

Without the plugin, register the MCP server by hand:

claude mcp add --scope user --transport stdio cc-context -- ccx mcp

Use cases

Orient in an unfamiliar repo in one command

Need the lay of the land before a deep dive? One command draws the map:

ccx repo overview
# cc-context — go module github.com/yasyf/cc-context (go 1.26)
languages: go (217), py (67), md (31), rs (13), sh (5)
dirs: internal (31 pkgs: anchor, astgrep, backend, cache, …) · bench (4 pkgs: analysis, ccxbench, tasks, tests) · format-core (3 pkgs: core, corpus, wasm) · plugin (4 pkgs: agents, hooks, scripts, skills) · docs (2 pkgs: assets, scripts) · cmd/ccx · scripts
entry: cmd/ccx/main.go
manifests: go.mod (15 direct deps)
tests: 123 test files (go, py)
git: @ cf13457c "fix(lint): gosec/revive/staticcheck cleanups in read and overview" · 46 dirty · 262 commits
hot (90d): bench/tasks (233), plugin/hooks (167), internal/cli (154), bench/ccxbench (123), internal/web (105)

Structure, entry points, manifests, recent churn, and VCS state in a few hundred tokens. Skip it when you already know what you're looking for — a targeted search or symbol lookup beats a tour.

Locate one symbol without paging through the file

Where does this function live, what's its contract, and how big is its blast radius? One call answers all three:

ccx code symbol NewRootCmd
# symbol NewRootCmd — function  internal/cli/root.go:18-46#dezd
func NewRootCmd() *cobra.Command

NewRootCmd builds the root command and registers its subcommands.

refs 36 · tests 22 · siblings 11 — --callers/--tests/--siblings/--body/--full

Location, signature, and doc land in ~60 tokens; the counts trailer says whether expanding is worth a second call, and --callers, --body, or --full pull exactly the layer you need.

Keep JSON output out of context

gh --json and kubectl -o json dump verbose, nested JSON that can swamp a context window. Run the command through ccx format and its JSON or NDJSON stdout comes back re-encoded in the leanest accurate shape:

ccx format -- gh release list --limit 5 --json tagName,publishedAt,isLatest
|isLatest|publishedAt|tagName|
|---|---|---|
|true|2026-07-04T02:11:12Z|v0.4.0|
|false|2026-07-04T00:02:50Z|v0.3.0|
|false|2026-06-26T01:05:28Z|v0.2.1|
|false|2026-06-23T07:52:46Z|v0.2.0|
|false|2026-06-21T09:21:47Z|v0.1.1|

That's 40% fewer bytes than the raw JSON; grow the same table to 400 rows and it comes back as CSV at 60% off. A classifier reads the payload shape and picks among markdown table, CSV, TOON, TRON, JSONL, compact JSON, and, for prose-dominant payloads, the prose itself. Auto output never exceeds compact JSON by bytes; --format=X forces one encoder even when it's larger. Non-JSON output passes through verbatim, stderr streams live, and the exit code is propagated. The MCP BashFormat tool is the same wrapper in tool form.

Fan out a dozen calls, get six lines back

Every command above bounds or compresses the output of a single call. ccx exec goes one tier further. It runs a short Python script in a sandbox whose async host functions are every ccx query op, a gated sh(cmd), and the tools of every stateless MCP server registered with Claude Code. The script fans out calls, filters in the sandbox, and returns one value; only that value enters context, run through the same shape classifier as ccx format and capped at --budget:

ccx exec '
import asyncio
import re
async def main():
    raw = await outline("internal/cli")
    cmds = sorted(set(re.findall(r"func (new\w+Cmd)", raw)))
    return {"subcommands": len(cmds), "constructors": cmds}
asyncio.run(main())
'
{"constructors":["newCodeCmd","newDepsCmd","newDiffCmd","newExecCmd","newFindCmd","newFormatCmd","newGrepCmd","newHistoryCmd","newLocateCmd","newMCPCmd","newOutlineCmd","newOverviewCmd","newReadCmd","newRelatedCmd","newReplaceCmd","newRepoCmd","newSearchCmd","newShipCmd","newShowCmd","newSymbolCmd","newVcsCmd"],"subcommands":21}

The ~11,000-character outline of 35 files stayed in the sandbox; only the answer came back. In the spike's four replayed agent episodes, that pattern cut the characters entering context by 12-99×, measured as a raw character delta, separate from the benchmark suite — where agents never reached for exec unprompted. It pays when your prompts or project guides steer composition through it. The full measured story — where ccx costs tokens, where it saves them, and where it wins on accuracy instead — lives in bench/FINDINGS.md. Scripts use a restricted Python subset; ccx exec --list-tools prints the host-function catalog and the full rules. The MCP facade exposes the same surface as ccx_exec.

The guard pack enforces it

A budgeted command only helps if the agent reaches for it. The bundled capt-hook guard pack makes that the path of least resistance. Its PreToolUse/PostToolUse hooks rewrite simple token-heavy commands in place — cat runs as ccx code read, a sed line range as --section, a literal grep (or a dialect-safe regex one, carried through as --regex) as ccx code grep, bare git diff/git show/git log -p as their ccx vcs equivalents, an unpiped curl page dump as ccx web read — each with a note saying what ran instead. A grep naming explicit files is rewritten when ccx can map it, and otherwise passes through only while those files stay under the large-read size threshold; over that threshold, or on a tree-wide shape with no faithful mapping (an exotic regex, an exit-code grep -q, an untranslatable flag), it blocks with a pointer at the ccx equivalent, and an unbounded full-file Read gets a hundred-line window plus a steer to ccx code outline. Whole-page WebFetches keep the hard block — a hook can't swap one tool for another — and the block message names the drop-in instead: spawn the bundled cc-context:web-fetch agent with the same URL and prompt, and only the cited answer enters context. A deliberate re-run of the same URL still passes, so pages ccx web can't serve stay reachable. It also watches for JSON. A command flagged for JSON output (--json, -o json) gets rewritten to run through ccx format, and the pack learns which commands emit JSON so it can nudge you to wrap them next time.

When Claude Code defers MCP tools behind tool search (ENABLE_TOOL_SEARCH), the everyday tools — ccx_code_read, ccx_code_grep, ccx_code_outline, ccx_code_search — are marked alwaysLoad, so they stay in the prompt from the first turn and a guard redirect to one costs no tool-search round-trip. The rest of the surface stays deferred, loaded on demand.

Commands

Each command is a token-bounded stand-in for a primitive an agent would otherwise reach for. Structural output is capped at --budget tokens, cut on a line boundary, with an explicit marker saying how much was dropped. The daily drivers:

Command What it does
ccx repo overview Repository structure and entry points, for untargeted starts
ccx code search <query> [path] Search routed by query kind; natural language runs semantic, an ast-grep pattern ($A, $$$) runs structural
ccx code symbol <name> (alias grok) Location, signature, doc + caller/callee counts; flags expand body, callers, tests
ccx code outline <file-or-dir> Token-budgeted structural outline of a file or directory
ccx code read <file> --section A-B Read a line range or a ## Heading instead of the whole file
ccx vcs diff [uncommitted|staged|<ref>] VCS-aware structural diff; defaults to uncommitted
ccx web outline <url> Heading tree of a web page with stable § section refs
ccx web read <url> --section <ref> Read one section of a page, with prev/next nav, instead of the whole thing
ccx web search <url> "<question>" Ask a page a question; top-k relevant chunks with § cites
ccx format [-- <cmd>] Re-encode a command's JSON/NDJSON output lean, or filter a pipe
ccx exec [script] Compose ccx ops, sh(), and reflected MCP tools in a sandbox; only the return value enters context

Every command that narrows by path takes one selector: -g/--glob, repeatable and ordered, with ripgrep's semantics — a leading ! excludes, the last match wins, and any include turns the set into a whitelist. ccx code grep "func New" -g 'internal/**' -g '!*_test.go' searches a subtree minus its tests; ccx repo find takes the same globs as positionals. Two rules are worth knowing before you hit them. A bare directory means its subtree — -g internal normalizes to internal/**, a deliberate divergence from raw ripgrep, where a bare directory name matches no file. And a glob anchored at a real directory is searched even under ignore rules (-g '.venv/**/*.py'), but only when every include shares that anchor: mix -g '.venv/**/*.py' with -g '*.sh' and neither anchors, so the ignored tree drops out silently. Put both extensions under the one anchor (-g '.venv/**/*.{py,sh}') or make two calls.

ccx --help catalogs the rest, including structural find-replace with a preview-first --apply, dependency maps, per-commit symbol history, and ccx vcs ship -m "<msg>" [paths...] to commit, push, and watch CI in one call — it runs the repo's prek pre-commit hooks first via uvx prek (auto-fixes fold into the commit, a persistent failure aborts before anything lands, --no-verify skips) and exactly once — a commit ship has already hooked carries --no-verify, so git's own pre-commit hook doesn't run the same checks a second time, while a repo ship didn't hook keeps its git hooks — trailing paths scope the commit to just those files, ccx vcs hunks refs with --skip-hunk/--only-hunk scope it to individual hunks of a file, where the commit goes resolves before any mutation (a non-trunk branch gets appended to, trunk stays direct in your own repositories and becomes a branch named from the commit subject when GitHub says the repo is someone else's, and --branch/--new-branch/--append state intent explicitly), a remote that advances mid-ship is absorbed in-call (fetch + rebase with a bounded retry on a rejected push; a conflicting rebase rolls back and reports), and commits made from a Claude session carry a Claude-Session-Id trailer; ccx <command> --help has the flags. In a Graphite-managed repo (a live .git/.graphite_repo_config, linked worktrees included) ship switches lanes: commits go through gt create/gt modify and land as submitted stacked PRs — published by default, --draft for drafts, --no-gt to opt out — though the lane is gated before any mutation: a repo that sets ccx.nogt, one GitHub says is someone else's, or one Graphite reports it cannot submit to falls back to jj/git with the reason in the report, and ccx vcs info (alias lane) prints the same verdict, and why, without committing anything — along with where a linked worktree sits in its repository and which working copy currently holds trunk, and it reports a checkout it cannot resolve instead of refusing. Ship also owns the pull request in every lane: --pr-title and --pr-body-file (repeatable, branch-scoped as <branch>=<value>; - reads stdin) create the branch's PR or edit exactly the fields restated, --draft/--publish convert an existing PR in either direction, --no-pr opts out, and a ship naming no PR flag pushes exactly as before; ccx vcs guidelines (alias contributing) fetches and caches the repo's PR templates, CONTRIBUTING.md, code of conduct, and issue config, each served verbatim, never summarized. ccx vcs reviews attaches to a PR and streams each new review comment until it merges or closes (ship --reviews chains the same watch after CI), and ccx vcs restack (alias rebase) brings the stack current with trunk on whichever backend the repo uses — gt sync, a jj rebase onto trunk(), or git rebase --autostash — refusing with recovery steps on conflict. Two external engines sit behind the one surface — ast-grep for structural search, rewrites, and outlines, and ripgrep for every text grep (system grep fills in when rg is missing) — and everything else runs natively inside ccx: read, symbol, deps, diff, history, overview, and web are computed fresh from the working tree on every call, and semantic search embeds and ranks in-process (model weights download once into a local cache on first use). ccx routes each command for you.

Configuration

ccx reads no config file; behavior is tuned through environment variables:

Variable Effect
LOG_LEVEL / LOG_FORMAT debug, info (default), warn, or error, to stderr; set LOG_FORMAT=json for structured logs
CCX_EXEC_MCP off disables MCP auto-reflection in ccx exec; refresh forces a fresh claude mcp list probe, bypassing the 15-minute per-project inventory cache
CCX_EXEC_MCP_DENY comma-separated MCP server names to exclude from reflection; reflected servers run as fresh instances, so list any that need live session state
CCX_EXEC_MCP_ALLOW comma-separated MCP server names to reflect even when classified stateful
CCX_EXEC_MCP_TIMEOUT Go duration bounding the claude mcp list probe (default 30s); a probe that fails past the cache TTL falls back to the last good inventory

Licensed under PolyForm Noncommercial 1.0.0.

About

Take `cat` away from your agent

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages