Imported from yasyf/cc-context (
plugin/skills/ccx/SKILL.md). Install upstream withnpx skills add yasyf/cc-context --skill ccx. Copyright stays with the author.
ccx — compact codebase context
ccx answers codebase questions with bounded, structured output: line-numbered,
budget-capped, explicit about overflow, never silently truncated. Across two 450-run
benchmark campaigns that structure matched or beat native-tool accuracy on locate,
trace, and diff questions, and held after the terse defaults (CLI 100% on the gated
re-run; MCP 96.0% vs baseline 96.2%). Use it as the first stop for a targeted
question: a file section, a symbol, a search, a diff. One boundary: when the answer
must be a complete set ("every subclass of X", "every module importing Y"), read the
candidate files — bounded views can withhold members, and measured accuracy reversed
on exactly those enumeration tasks.
The MCP tools mirror the ccx query surface: ccx_repo_overview,
ccx_code_search, ccx_code_outline, and the rest of
the read, search, edit, and diff commands take the same arguments as their CLI counterparts;
ccx_exec and ccx_exec_tools mirror ccx exec; BashFormat mirrors
ccx format -- <cmd>. Clients may show these under a client-assigned mcp__…__
prefix; use the names as listed in the tool inventory.
Use whichever is available; the workflow is identical. ccx vcs ship, ccx vcs hunks,
ccx vcs show, ccx vcs history, ccx vcs info, ccx vcs status, ccx vcs guidelines, and
ccx repo locate are CLI-only — there is no MCP tool for them.
Workflow
1. Orient
Facing an untargeted task in an unfamiliar codebase? Draw the map:
ccx repo overview
It reports the structure, languages, and entry points, enough to know where to look before you look. Skip it when you already know what you're after — starting with a search or symbol lookup costs one round-trip less than starting with a tour.
2. Find
Pick the tool by what you know.
- Have the intent but not the name. Semantic search by meaning — a native in-process
engine, no external service.
--contentnarrows the corpus (space-separatedcode|docs|config|all, defaultcode docs), the same flag onccx code relatedand over the MCP:ccx code search "how requests get authenticated" ccx code search "retry backoff policy" --content "code docs config" - Have the exact symbol. Location, signature, and doc, with a counts trailer;
--callers/--callees/--body/--fullexpand the layer you need:ccx code symbol authenticate # alias: ccx code grok authenticate - Have literal text to match. Error strings, env-var names, a verbatim token:
ccx code grep "RATE_LIMIT" ccx code grep "func New" -g "internal/**/*.go" ccx code grep -i -w "ratelimit" # case-insensitive, whole-word (runs on ripgrep) ccx code grep TODO cmd/main.go internal/cli/run.go -g "*.go" # -g filters within explicit paths - Have a path shape. List files with per-file token counts — gitignore-honoring,
VCS stores skipped, sorted, budget-capped (
--budget Nraises the cap):
Orienting a whole repo isccx repo find "internal/**/*.go"ccx repo overview, not a"**/*"listing.
Narrowing with -g
-g/--glob is the only path selector. It is repeatable and ordered, and one set of
rules governs it on code grep, code symbol, code replace, vcs diff, vcs show,
and repo find (where the globs are positionals). There is no --scope — passing one
is unknown flag.
ccx code grep "func New" -g "internal/**" -g "!*_test.go" # subtree, minus tests
ccx code symbol Cap -g "internal/render/**" # was --scope internal/render
ccx vcs diff -g "internal/cli/ship.go" # one changed file
ccx repo find "*.go" "!vendor/**" # positional on find
The rules are ripgrep's, and MatchGlobs applies them wherever ccx filters in Go:
- Order decides. Last match wins; any include turns the set into a whitelist, an exclusion-only set keeps everything it does not exclude.
- A slash changes the target. A slash-less glob (
*.go) matches the basename at any depth; a slashed one (internal/*.go) matches the path, so it selects direct children only. That path is cwd-relative on the search ops and repo-root-relative onvcs diff/vcs show, which filter git's own changed-file list — so run from a subdirectory, those two want the repo-root spelling (-g 'pkg/sub/*.go', not-g 'sub/*.go'). - A bare directory means its subtree.
-g internalnormalizes tointernal/**. Raw ripgrep matches nothing against a bare directory name; ccx diverges on purpose, so-g <dir>is the drop-in for the old--scope <dir>. - An anchored glob escapes the ignore rules.
-g '.venv/**/*.py'searches an ignored tree, because the literal anchor becomes the search operand. - Mixed anchors fall off the cliff. The anchor is peeled only when every include
shares it — ripgrep applies each
-gunder every operand, so peeling disjoint anchors would silently widen the search.-g '.venv/**/*.py' -g '*.sh'does not anchor, and the ignored.venvtree drops out with no warning. Recover with braces under one anchor (-g '.venv/**/*.{py,sh}') or with two calls.
Migrating a --scope: --scope D is -g D; --scope D --glob G is -g 'D/**/G'
for a slash-less G or -g 'D/G' for a slashed one; a file-valued --scope F is a
positional F on code grep and -g F on vcs diff/vcs show. code deps had a
--scope that nothing read, and it is gone with no replacement.
3. Read
Outline before you read. The outline is the structure with line numbers, each span
carrying a content anchor (L15#k2fa), bounded to a token budget:
ccx code outline internal/router/router.go
Then read only the span you need. Echo an anchor back from any producer command, or pass a plain line range or a heading:
ccx code read internal/router/router.go --section 40-95#k2fa # anchored span from outline
ccx code read README.md --section "## Workflow"
When you need the whole file, because it is small or the outline says so, escape the budget:
ccx code read internal/router/router.go --full
4. Edit
Write through the same anchor you read with. The hash is the verification: ccx code edit (MCP: the ccx_code_edit tool) refuses to write unless the anchored
content still matches. A span that merely moved re-anchors, applies, and prepends
# anchor k2fa: line 40 → 44; a vanished or ambiguous anchor errors before any write,
leaving the file byte-identical.
ccx code edit internal/router/router.go --at 40-43#k2fa --content 'func route(p string) Handler {
return lookup(p)
}'
The report maps the old cite to the new (40-43#k2fa → 40-42#s45e, plus a -/+ diff
of the span); the returned anchor chains into the next edit without a re-read. There is
no preview round-trip — code replace previews because a structural pattern can
over-match, but an anchor names exactly one verified site, so the edit applies
immediately. --content - reads stdin (a single trailing newline terminates the last
line); --delete removes the range instead; a plain numeric --at A-B is legal but
unverified. Untouched lines round-trip byte-identical — CRLF and a missing trailing
newline survive, and the file mode is preserved.
When you know the exact text but not its span, --match addresses by content instead:
ccx code edit internal/router/router.go --match 'return lookup(p)' --content 'return lookup(canon(p))'
The needle is byte-exact — trailing spaces, tabs, and multi-line spans all count, with
newlines matched to the file's EOL convention — and the content is written verbatim:
nothing is trimmed, and a trailing newline lands on disk. Zero matches error before any write; several error listing each candidate's
line#hash, so the recovery is --at 40#k2fa --match …, which confines the scan to
that verified span. --all replaces every occurrence and reports one stanza per match,
refs in final-file coordinates. The report echoes each written line back with its fresh
anchor — the + rows are what the file now contains.
5. Review
Inspect changes without dumping the entire working tree:
ccx vcs diff # uncommitted
ccx vcs diff staged
ccx vcs diff main # against a ref
Inspect a single commit — its message plus a structural per-file diff — with show,
and trace how one file changed across its recent commits with history:
ccx vcs show # the last commit (@-/HEAD)
ccx vcs show a1b2c3d # a named commit
ccx vcs history internal/cli/root.go # per-commit sha · date · subject + changed symbols
ccx vcs history internal/cli/root.go -n 5 # cap the commit count
6. Locate
Resolve a repo, Go module, or Python package to its on-disk path instead of scanning
~/Code or the module cache by hand:
ccx repo locate captain-hook # a sibling repo under ~/Code
ccx repo locate github.com/spf13/cobra # a Go module in the cache
Each match prints a tab-separated kind path version line — one per cached module
version — and the command exits 3 when nothing resolves.
7. Ship
Commit, push, and watch CI in one call. ship runs a jj-aware commit (plain git
otherwise), pushes, then watches every workflow run on the pushed commit — found via
gh run list --commit, retrying registration for up to a minute. Trailing paths
scope the commit to just those files; the rest of the working copy — a concurrent
session's edits included — stays put. Where the commit goes is one decision,
resolved before any mutation and reported as a branch <name> or created <name>
segment: a non-trunk branch or bookmark gets appended to, trunk stays direct in your
own repositories and becomes a branch named from the commit subject when GitHub says
the repository is someone else's, and a detached HEAD or an ambiguous trunk refuses.
--branch <name>, --new-branch[=<name>], --append, and --allow-trunk state
intent explicitly (--bookmark is the jj-only alias of --branch, --create the
deprecated alias of --new-branch).
A branch someone else already committed — a delegate's worktree, a git commit by
hand, a codex lane — ships without a flag: with nothing left to commit over commits
trunk does not carry, ship skips the commit and goes on to track, push, and the PR,
reporting nothing to commit — shipping as --no-commit. Trailing paths do not
block it: a scoped ship whose paths come up clean takes the same path, the report
naming the paths. --no-commit states that path outright, takes no paths
(--no-commit takes no paths — a path scopes a commit, and --no-commit cuts none),
and refuses changes to tracked files rather than leave them out of the branch and
the PR. Git's untracked paths do not refuse it — a run cutting no commit carries
no scratch into one — and the report names them as left untracked: <paths>; jj
has nothing to exempt, a new file there already being part of @. A branch level
with trunk still refuses: there is nothing to submit either way.
A live Graphite config (.git/.graphite_repo_config, linked worktrees included)
routes ship to the gt lane — commits through gt create/gt modify, the push one
atomic force-with-lease of every branch and one submit call per branch over
Graphite's API, published by default — after three gates, each
demoting to jj/git with the reason in a leading lane <kind> (<reason>) segment:
ccx.nogt set in git config, GitHub positively saying the repo is someone else's,
and a cached probe of whether Graphite can actually submit here (a probe that
times out or cannot reach Graphite demotes too, and each verdict caches for its
own span: a yes a day, a no an hour, an unanswered probe a minute). --no-gt
opts out. Ship owns the pull
request in every lane: --pr-title/--pr-body-file (repeatable, branch-scoped as
<branch>=<value>, a bare value applying to the tip, - reading stdin) create the
branch's PR or edit exactly the fields restated — a hand-edited description
survives a re-ship that does not mention it — --draft/--publish convert an
existing PR in either direction, --no-pr opts out, and a ship naming no PR flag
makes no gh pr call. -m is optional when an unscoped --pr-title is given: the
title becomes the commit subject and an unscoped --pr-body-file its body, with the
<details> wrapper dropped and each ## Heading folded into a Heading: paragraph.
On the gt lane an unrestacked downstack is recovered rather than refused — ship
commits, then rebases every branch of the chain sitting off its parent with git replay, which moves the refs without checking a branch out, so a branch a
sibling working copy holds is not a special case; each holder is then reset onto
its new head, its uncommitted work snapshotted beforehand and applied after,
and a branch gt freeze is holding is left where it is. Only a
conflict stays manual, and the refusal names the gt restack --only --branch <b>
and ccx vcs ship --no-commit steps back, since the commit has landed by then. --yolo is the one switch for "skip the checks": it implies
--no-verify and drops every guard ship adds of its own, of which there are none
today; it never drops a refusal git or gt would make anyway.
The first line is the summary; each watched run adds
a workflow · conclusion · duration · url line,
and a red run adds its failing jobs and a budget-capped --log-failed excerpt with a
full log: pointer. CI failure means a run went red; CI error means the watch
itself failed after a successful push (the check: line says how to resume — don't
re-run ship, that cuts a new commit). On a terminal, progress streams live:
ccx vcs ship -m "fix: budget overflow marker" # commit + push + watch CI
ccx vcs ship -m "fix: route" internal/cli xs.md # scoped: commit only these paths
ccx vcs hunks f.go # list pending hunks as file:A-B#hash refs
ccx vcs ship -m "fix: x" --skip-hunk f.go:530-536#k2fa f.go # ship f.go minus one hunk
ccx vcs ship -m "wip" --no-push # commit only, skip push and CI
ccx vcs ship --amend # fold the working copy into the parent
ccx vcs ship -m "spike" --branch me/probe # commit onto a named branch
ccx vcs ship -m "feat: x" --new-branch --pr-title "Add X" --pr-body-file body.md
ccx vcs ship --new-branch --pr-title "Add X" --pr-body-file body.md # -m derived from them
ccx vcs ship --pr-title "Add X" --pr-body-file body.md # already committed: just submit it
ccx vcs ship -m "fix: x" internal/cli # paths already committed: submits it too
ccx vcs ship -m "fix: x" --budget 0 # uncapped failure-log excerpt
ccx vcs info (alias lane) reports the lane a mutating command would take and
why — vcs kind, branch, trunk, the Graphite gates, the GitHub repo record, and on
the gt lane the downstack with each branch's PR: whether it has a body, how it
ended, and its check rollup, in one batched query for the whole stack. The
Graphite merge queue squash-merges a stack into one trunk commit, so a PR it
landed reports CLOSED with no mergedAt; on the gt lane the report calls that
merged, where reading the state alone reports a successful merge as an
abandonment. A linked worktree also reports
where it sits: its shape, the repository's own working copy, the common dir its
siblings contend over, and the key that separates siblings from unrelated repos.
Every git-backed checkout reports which working copy holds trunk, which is what
explains a gt restack that skipped a branch and still exited 0. Inputs it cannot
read — a gitdir pointer resolving to nothing, graphite state it cannot walk, an
unreachable GitHub — are reported in the report rather than raised as errors.
ccx vcs status answers the next question — what the whole stack is waiting on —
in one batched query plus, only where the merge queue has been involved, two
narrow follow-ups. Per branch, in stack order: its divergence from trunk, whether
gt says it sits off its parent, its PR's mergeability (re-asked once when
GitHub's first answer is UNKNOWN, which it usually is), the checks on the head
and which of them the base branch's protection rule requires, and a blocked
line per hold. Every standing review is named with the commit it was cast on: an
approval three pushes stale still counts toward reviewDecision. CI is read
through statusCheckRollup, never gh run list, so a repository on Buildkite
reports its pipeline rather than "no CI".
It also names the commit the Graphite merge queue is holding. The queue snapshots a PR when it admits it, so a push after that lands the older commit and drops the rest, silently, with every check green. No GitHub field carries the snapshot, so status reconstructs it from when the queue last looked — a merge label going on, and every draft PR the queue opened — against the branch's own head history, and names the commits a merge would leave behind. The same activity comment is what separates a label the queue consumed on admission from one it dropped on failure, which are otherwise the same absent label.
That comment is not always posted — PRs enter the queue and merge without one —
so a queue-gt line carries gt's own verdict from gt merge --dry-run, which
reports what would merge and terminates without merging anything, and says
The stack is already merging where nothing else does. --no-queue-probe skips
it. Two more facts this repo's queue taught the command: it closes what it
merges, so a landed PR reads CLOSED with a null mergedAt and status calls
that merged on the gt lane rather than abandoned; and its stack metadata is
repo-global, so concurrent gt runs in sibling checkouts can reparent a branch
onto another lane's tip — a gt parent that disagrees with the PR's own base is a
blocker, and the PR's changed-file count is on the pr line, because a diff
wider than the work is the tell.
ccx vcs guidelines (alias
contributing) fetches and caches the repo's PR templates, CONTRIBUTING.md, code
of conduct, and issue config, served verbatim so a PR body can reproduce the
template exactly:
ccx vcs info # which lane a ship would take, and why
ccx vcs status # every branch, its PR, and what blocks it
ccx vcs status --json # the same report as a structure
ccx vcs guidelines # PR templates + contribution rules, verbatim
8. Re-encode
JSON tool output enters context through ccx format — the default wrapper for any
command that emits JSON or NDJSON (gh --json, kubectl -o json, terraform output -json), and a filter for pipes:
ccx format -- gh pr list --json number,title,author
kubectl get pods -o json | ccx format
A classifier reads the payload's shape and emits the leanest accurate encoding:
| Payload shape | What you get |
|---|---|
| Under 200 bytes | Compact JSON — format deltas are noise at this size |
| Prose-dominant (one text field at ⅔ of the payload, or any 2 KiB+ prose field) | The prose itself, other fields as XML-ish metadata tags |
| Uniform array of objects, small | Markdown table |
| Uniform array of objects, large | CSV/TSV byte shootout; TOON enters at 100+ rows and wins only when more than 5% smaller |
| Repeated nested shapes | TRON — class declarations for the repeated key-sets |
| Heterogeneous or log-like array | JSONL |
| Anything else | Compact JSON |
Near-ties go to the classifier's preferred encoding — a later candidate must beat an
earlier one by more than 5% in bytes to displace it. Auto output never exceeds
compact JSON by bytes; --format=X forces one encoder even
when it's larger. Non-JSON output passes through verbatim and the exit code is
propagated. Over MCP, the BashFormat tool runs the command and returns the
compacted output — a format param forces an encoder.
9. Web pages
Web pages get the file treatment: outline first, then read one section, or ask the page a question instead of paging through it top to bottom.
ccx web outline https://example.com/docs
ccx web read https://example.com/docs --section 2.3
ccx web search https://example.com/docs "how do I configure retries"
outline returns the heading tree with stable section refs. Echo one into ccx web read --section for that section's subtree with a §prev/§next footer, or take the
whole page with --full. search answers with the top-k relevant chunks, each carrying
a <url> §2.3#k7fq cite whose ref echoes back into read; ranking is hybrid BM25 +
local embeddings, degrading to BM25-only (and saying so) when uv is off PATH.
On a page with no heading structure the outline collapses to one section — lead with
search, or page through it with read --offset <tokens>, echoing the next offset
from the overflow footer.
A page that fetches as a JS app shell — near-empty content from a client-side app —
escalates automatically to a rendered fetch when a lane is available: keyed jina
(JINA_API_KEY), firecrawl (FIRECRAWL_API_KEY), or the agent-browser CLI on PATH.
The jina lane also appends the page's links as a ## Links section, so nav slugs land
in outline and search. With no lane available the thin copy is served with a note naming
what to set or install — set it, then re-run with --refresh.
Fetched pages and their indexes persist in the ccx cache for 24 hours — --refresh on
any of the three bypasses the TTL. The MCP mirrors are ccx_web_outline,
ccx_web_read, and ccx_web_search.
A one-shot question about a page can skip the loop entirely: spawn the
cc-context:web-fetch agent with the URL and your prompt, and only the cited answer
enters your context — see Reader subagents.
10. Compose
One question takes one call from steps 1–9. When the work is a pipeline — two or more
chained calls, output you'd immediately filter or project, a fan-out across files —
write the pipeline as a script instead. ccx exec (MCP: the ccx_exec tool)
runs a short Python script in a sandbox where every ccx query op above is an async host
function, alongside a gated sh(cmd) and the tools of every stateless MCP server,
auto-reflected with no flag needed. Intermediate output stays in the sandbox; only the
script's return value enters context. A shell pipe is not the bound: ccx output is
already budget-capped, and | head/| tail re-introduces silent truncation while
eating the overflow footer — raise --budget, narrow with --section/-g, or
write the exec script.
ccx exec 'import asyncio
async def main():
raw = await grep("TODO", globs=["internal/**/*.go"])
return [ln for ln in raw.splitlines() if "FIXME" not in ln]
asyncio.run(main())'
The script comes in as an argument, --file <path>, or stdin (--file -); --budget
caps the result size.
- Print the catalog once per session, before the first script.
ccx exec --list-tools(MCP: theccx_exec_toolstool) lists every host function signature — ccx ops,sh, and the reflected MCP tools — plus the Python-subset rules and a worked example. Once is enough; don't re-run it per script. - The sandbox speaks a Python subset. No classes, no
match; imports are limited tore,json,datetime, andasyncio, one module perimportline. A top-levelreturnis illegal — wrap the logic inasync def main()and end the script withasyncio.run(main()). Every host function is async: await it, and run independent calls concurrently withasyncio.gather(...). - Reflected servers are fresh instances. Each stateless MCP server is spawned anew
for the sandbox, so a tool that needs live session state will misbehave — exclude it
with
CCX_EXEC_MCP_DENY=<name>(comma-separated;CCX_EXEC_MCP_ALLOWoverrides the classifier the other way, andCCX_EXEC_MCP=offdisables reflection entirely). - Discovery is cached per project for 15 minutes. A script that references no
reflected tool skips reflection entirely — no probe, no notes. Changing
CCX_EXEC_MCP_ALLOW/CCX_EXEC_MCP_DENYinvalidates the cache; after adding an MCP server, force a fresh probe withCCX_EXEC_MCP=refresh.CCX_EXEC_MCP_TIMEOUT(Go duration, default30s) bounds theclaude mcp listhealth check.
Guarantees
These hold for every command, which is what makes ccx safe to trust over a raw read:
- Spans stay valid, or report they moved. Every span you get back carries a short
content anchor, like
L15#k2fa. Echo it intoccx code read --section 15-27#k2faand it resolves by content, not by line count. An exact hit comes back silently, a shifted span re-anchors and prepends# anchor k2fa: line 15 → 22, and vanished content errors, telling you to re-runccx code outline. - Anchored cites outlive the file. Durable prose — plans, reviews, memory files —
may cite code as
path:line#hash(e.g.internal/render/finalize.go:31#k2fa). Resolution is stateless: any later session resolves the cite, even after the file has drifted, because the hash re-anchors by content — which is why anchored cites beat bare line numbers in anything durable. The same check gates writes:ccx code editrefuses to touch a span whose content no longer matches its anchor. - Budgets are enforced. Structural output is capped to the token budget, and most results report their own size — you know what a result cost before reading more.
- Overflow is explicit. When a result exceeds the budget, ccx says so and tells you what it left out. It never silently truncates.
- There is always a raw fallback. Hit overflow or want the unabridged source and
you have an escape hatch. Use
ccx code read --fullfor a whole file, a path-scopedccx vcs diff <ref>for changes, orReadwith anoffsetfor a known line range. - Exec returns are budget-capped. A script can touch megabytes across dozens of
calls; only its return value comes back, run through the
ccx formatshape classifier and capped to the token budget like any other command. sh()is a sanctioned bypass of the read guards. It runs host-side inside the ccx process, out of the guard hooks' sight, with its own denylist ininternal/codeexec/sh.go. That is safe because only the script's filtered return enters context, never the raw command output.
Why ccx first
The guard hooks block token-heavy primitives: a full-file Read of a large file, a
broad git diff, raw grep, sed -n, a bare cat, ls -R, find enumeration, a
whole-page WebFetch, and an unpiped curl/wget page dump (a URL ccx web can't
serve stays reachable — the same WebFetch passes on a deliberate re-run).
Each has a ccx equivalent that returns a bounded, structured answer — line-numbered,
budget-capped, explicit about what was cut — where native output can be unbounded and
says nothing about what's missing. Measured head-to-head across two 450-run
campaigns, that structure bought correctness: ccx arms matched or beat native
accuracy on locate/trace/diff tasks; under the terse defaults the CLI scored 100% on
the gated re-run (MCP: 96.0% vs baseline 96.2%); and the structural diff caught a
misattributed hunk that raw hunks missed. Reach for ccx on targeted questions; reach
for the raw tool and the hook turns you back anyway.
The honest exception is exhaustive enumeration. When the deliverable is a complete
set, compact views can withhold exactly the members you need — measured on
enumeration tasks, ccx-cli's accuracy fell to 76.9% against the baseline's 93.3%.
Read the candidate files (sectioned or --full) and treat ccx as the locator, not
the enumerator — or spawn the enumerator agent below, which packages that
verification loop.
Reader subagents
The plugin ships five read-only agents that run the flows above in their own
context and return only conclusions. Spawn them as cc-context:<name> whenever
the raw material — a web page, a CI log, a dependency's source — has no business
entering yours:
web-fetch— the WebFetch drop-in: one URL plus your prompt in, the cited answer out. The WebFetch and page-dump guard messages name it.web-researcher— a research question plus seed URLs; discovers pages with WebSearch, reads across them viaccx web, returns findings with a<url> §refcite per claim.ci-triage— a red run id; digsgh run view --log-failedwhere it can't flood anything and returns the root cause, a minimal excerpt, and the next step.ccx vcs ship's red-run report points at it.dep-reader— "how does<pkg>implement X"; resolves the installed source withccx repo locateand returns the answer withpath:line#hashcites.enumerator— complete-set questions ("every subclass of X"); sweeps LSP, call-graph, and textual lanes, verifies each candidate by reading it, and returns the proven set with its blind spots named.
All five default to model: sonnet; pass model: opus (or higher) at spawn
time when the page, log, or set warrants it.