ccx is the client CLI for CocoIndex Code Plus. It queries a codebase that a
remote query server has indexed — semantic search, AST structural grep,
symbol definitions/references, read-only file access, and (where enabled) a
cited written answer to a question — over HTTP. The CLI holds no index: results
describe the server's indexed snapshot, not your disk.
When to reach for ccx
Reach for ccx first — before grepping and reading files — whenever the task is to understand or find code. The server holds a resolved index of the whole repo (and of other refs and other repos), so it answers in one call what local tools reach only after a chain of greps:
- Fuzzy / conceptual search — you don't know the exact term, so text grep
has nothing to anchor on ("where is retry handled?", "how are embeddings
stored?") →
ccx search. - Structure-oriented grep — the question is about code shape, not text
lines: a def vs. a call, a
catchthat re-throws the same variable it caught, everyisinstanceon a type, a nested generic that>>breaks for regex →ccx grep. - Symbol navigation — "where is
Xdefined?", "who calls / imports / usesX?": a resolved cross-file answer (alias- and re-export-aware, not a text match) →ccx defs/ccx refs. Covers Python, TS/JS (incl. TSX), C/C++, C#, and Rust. - Large / remote / multi-repo corpus — the repo isn't checked out locally,
you need a branch or tag other than your checkout, or the question spans
several indexed repos (
--repo, repeatable forsearchandask;--git-ref). - A question, not a lookup — the deliverable is a written, cited
explanation ("how does re-embedding get decided, end to end?", "compare auth
in these two services"), or the question is too broad for one search or
pattern →
ccx ask(see below).
One command per question, from a checkout of the repo (no flags needed):
ccx defs <Name> # where is it defined — then paste the row's `uses:` command
ccx refs <Name> --role call # who calls it
ccx search "<what the code does>" # no exact term to grep
ccx grep '<pattern>' -l <language> # a syntactic shape
ccx ask "<question>" # a cited written answer
Local tools stay the right choice for exactly two things: reading a file that
is already at hand, and the files you edited or added since the indexed commit
(the snapshot rule below). A bare identifier is not a reason to grep: as a text
pattern ccx grep with no metavariable floods hits, but the question behind it
is almost always a symbol question — its definition, or its true use sites
rather than every textual occurrence — and ccx defs / ccx refs answer that
where rg cannot.
Repo & ref scoping (applies to every query command)
- Repo auto-detection. Commands auto-scope to the repo of the current git
checkout, detected from its
originGitHub/GitLab remote (resolved to an<owner>/<repo>name). Override with--repo <owner>/<repo>(repeatable forsearchandask, up to the server's per-search cap). Without such an origin the command errors with guidance rather than guessing — pass--repo;ccx reposlists the indexed repos you can target, each with the refs it is indexed at as<ref>@<commit>(pass the part before@to--git-ref). There is no global "search everything" mode. - Ref defaulting. Every query command is ref-scoped, and
--git-refis optional everywhere: when omitted, the server uses your checked-out branch if it's indexed, else the repo's default branch (an explicit--repoalways gets that repo's default branch) — and prints aUsing git ref …note to stderr so you know which ref answered (askprints itss0: … (commit …)scope lines instead). Trust this default; there is no need to runccx git-refsfirst just to discover a ref. Pass--git-refonly to target a different ref — a bare branch/tag name works (main,v1.2); the qualifiedheads/<branch>/tags/<tag>form is only needed when a branch and tag share a name. An unknown ref errors with the list of indexed refs. - The indexed ref is a commit snapshot. Results describe the code at that
commit, not your working tree — a
Using git ref …note naming your own branch still means the commit the index holds for it. So when your checkout has uncommitted or unpushed changes, every path and line number can be stale, and a symbol you added since that commit is absent — that is not evidence it doesn't exist. Read the note, and confirm a specific location in your working tree before acting on it; when you need the exact commit that answered,ccx git-refsprints it per ref. Keep querying on a dirty checkout, though: locating existing code — the common case — survives a line-number offset. On a feature branch the split is simple: ccx for the code your branch did not touch, which is nearly all of it; your own grep only for the files you edited or added since the indexed commit —git diff --name-only <indexed-commit>lists them, andccx reposprints that commit. - CWD subtree scoping. Run from a subdirectory of the checkout and
search/grepdefault--pathto that subtree (a stderr note names the glob). To cover the whole repo, run from the repo root or pass--path '*'. Don't mistake subtree-narrowed emptiness for "not in the codebase".
Semantic search
Describe the concept, behavior, or functionality to find — not exact syntax.
ccx search "how are vector embeddings stored" # auto-scopes to the current repo + branch
ccx search "user authentication flow"
ccx search "error handling retry logic"
- Scope.
--repo <owner>/<repo>targets another repo — repeat it to search several at once (one cross-repo-ranked list);--git-ref <ref>targets a non-default ref (single-repo scope only). - Filters.
--lang <language>restricts by source language and--path '<glob>'by path — both repeatable. - Results. Ranked by relevance — the most relevant come first, so if the
top hit already answers the question, stop there; don't fetch more just to be
safe.
-k/--top-k <N>returns more (default 5) and--offsetpaginates — raise-konly when every result still looks relevant (then there are likely more). An empty or off-target page is a signal to rephrase the concept, or to widen a subtree-narrowed--path— not to page further. - A hit shows that code talks about your question, not that it runs. Search ranks by resemblance to your words, so a doc, a default, a test, or an older or fallback implementation can outrank the code that actually does the work. "Where is X handled?" is settled by the hit itself — read it, stop if it does X. "Which code actually does X?" is settled by how the path uses the hit — as the thing it calls, or only as a fallback or one registered option — never by how well it reads. A caller already in the results may show it; otherwise see Settling what runs below.
ccx search "rate limiter" --repo acme/api --repo acme/worker
ccx search "connection pool sizing" --repo acme/api --git-ref main -k 10
ccx search parse config --lang python --path 'src/**'
Each result prints [score] <repo> <file> and the code with a line-number
gutter. To read more context around a hit, open the file with your normal file
tools (or, for a ref you don't have checked out, ccx read-file — see
remote-access reference).
AST structural grep
ccx grep matches a by-example pattern against the code's syntax tree (AST),
not text — so it ignores formatting and never matches code inside comments.
Requires -l/--language; --path '<glob>' narrows (repeatable); -k/--limit
(default 100) and --offset page, and a truncation note on stderr means there
are more matches.
ccx grep 'foo(\*)' -l python # foo(...) with any arguments: every call, and the def header
ccx grep 'def \_(\*) \*:' -l python # function defs (async, decorated, `-> T` too)
ccx grep 'isinstance(\_, \_)' -l python --path 'src/**'
The essentials, in one breath: write the code you're looking for, and replace the
parts that vary with metavariables — \ is the only special character. Five
forms cover nearly every pattern, and the two anonymous ones do most of the work:
| Form | Matches |
|---|---|
\_ |
exactly one node, any — a single slot (a receiver, one operand) |
\* |
a run of zero-or-more sibling nodes — the usual choice inside ( )/[ ]/{ } (\+ one-or-more, \? optional) |
\/re/ |
one node whose text matches the regex re (e.g. \/get_.*/) |
\NAME |
like \_, but named — only needed to report the capture, or reused later to require equal text (backreference) |
\{{ INNER \}} |
a node that contains INNER somewhere inside (any depth) |
Inside brackets, prefer \*: an argument list is several nodes, so
cached_fn(\_) matches only single-argument calls while cached_fn(\*) matches
them all. Reach for \NAME only when the name does work:
ccx grep '\_.filter(\*).map(\*)' -l rust # a .filter(...).map(...) chain, any receiver
ccx grep 'catch (\E) \{{ throw \E \}}' -l typescript # catch that re-throws the SAME var (backref \E)
ccx grep 'DenseMap<\_, \_>' -l c++ # nested generic; structural, so >> just works
Two mistakes to avoid:
- Never escape literal code — including inside strings.
\introduces pattern constructs; it is not a shell/sed/regex escape.class Call(\_):is right,class Call\(\_\):is wrong (\(…\)is the metavariable delimiter, so the escaped parens become a metavar → silently matches nothing). The same trap bites string content: write"..", not"\.\.". - Matching is at lexer-token boundaries — a string literal is one atomic
node.
\*and\NAMEcan't reach inside it, and a literal string in the pattern matches only the full literal:open("config")does not matchopen("app_config.yaml"). For partial string content use a regex metavar whose regex covers the quotes:open(\/".*config.*"/). And to find code that handles a concept — not one exact literal — reach forccx search, not a string-literal grep.
An empty result is information, not a near-miss. Structural match is literal about structure — a wrong guess returns nothing rather than something fuzzy. So when a grep comes back empty, loosen the structure, don't abandon it:
- Replace the parts you're least sure of with
\_/\*/\?, or a name you're unsure of with\/re/(e.g.\/get_.*/(\*)). - Unsure whether
Xis defined or only called here?X(\*)matches both the def header and every call site. - Check the scope — a stderr note about a CWD-subtree
--pathmeans you searched part of the repo;--path '*'widens. - Still nothing → the shape genuinely isn't there; pivot to
ccx searchfor the concept.
(Wrapping the whole pattern in \{{ … \}} does not broaden a top-level
match, and dropping to a bare identifier with no metavariable just floods hits.)
Key model: a pattern matches a fragment, child-aligned; incidental trailing
;/, are ignored, but closers (), }) are significant. The output shows
exactly the span the pattern covers — extend the pattern to see more:
def parse_config(\*) \*: prints only the header, while
def parse_config(\*) \*: \* prints the whole function including its body. That
\* before the colon absorbs a -> T return annotation — def f(\*): matches
only un-annotated defs (zero hits in a typed codebase), a silent miss. For the full
pattern language, verified recipes for common queries, and the gotchas (why
try \{{ … \}} needs the :, qualified names, fragment spans), read
references/grep-syntax.md before writing non-trivial
patterns.
Symbol navigation (ccx defs / ccx refs)
Where is a symbol defined, and who uses it — answered from a resolved
symbol graph (cross-file, alias- and re-export-aware), not from text matching.
Covers Python, TS/JS incl. TSX, C/C++, C#, and Rust; for other languages
fall back to grep/search.
ccx defs QueryService # definitions of the base name
ccx defs python:db.Repo.find # exact qualified name (the ':' selects the mode)
ccx defs Config --kind class --lang python # filter: --kind / --lang / --path
ccx refs QueryService # uses, by base name (broad recall)
ccx refs python:db.Repo.find # every use of this exact qualified name
ccx refs src/db.py Repo.find # uses of EXACTLY this definition (see below)
ccx refs QueryService --role call # only calls (roles: call, import, type_use, …)
The canonical flow is defs → refs, a copy-paste. Each ccx defs row's detail
line ends with a paste-ready command — uses: ccx refs PATH ENTITY_ID (carrying
--repo/--git-ref when you scoped explicitly); run it verbatim, adding
--role and friends as needed:
$ ccx defs find
src/db.py:42:4 [method] python:db.Repo.find
lang=python uses: ccx refs src/db.py Repo.find
$ ccx refs src/db.py Repo.find --role call
src/api.py:88:12 [call]
resolved → src/db.py Repo.find in handle_get
Three precisions of ccx refs, broad to exact:
- Bare name —
ccx refs NAME: every definition with that unqualified name, plus unresolved mentions. - Qualified name —
ccx refs python:db.Repo.find: the name accx defsheadline shows, every occurrence and overload under it. Every qualified name opens with its language tag (the CLI's errors say "pack-tagged"), so the:selects the mode — no flag needed. - One definition — the pasted
uses:command,ccx refs PATH ENTITY_ID.
Run the printed command — don't compose the second token yourself: the
headline's qualified name (python:db.Repo.find, module-qualified) is not
the entity id (Repo.find, file-relative). A lone argument that looks like half
of a forgotten pair (a path, or a dotted / #-suffixed id) errors with the fix
instead of running a broad query you didn't intend (--base-name forces name
mode when a base name genuinely contains such characters).
A dotted name without its tag (Repo.find, QueryService.search) is read by
defs as a base name and finds nothing: use the bare base name (find, with
--kind method to narrow) or the tagged name (python:db.Repo.find). Both
verbs page like grep (-k/--limit, default 100, and --offset); refs
has no --path, so set test files aside by eye.
Reading refs output — each row carries:
- a role (
call,import,type_use,inherit,implement,field_access,alias,decorates,use) — filter with--role; - a resolution:
resolvedis a definite reference, printed with its target in the same two-tokenPATH ENTITY_IDform, so any target you see pastes straight back intoccx refs;ambiguousmeans several candidates survived and each is its own row (the tool enumerates rather than guessing — so one source position can appear on several rows, and row count ≠ site count);name_only, printed→ ~name, is a mention whose target couldn't be resolved (an opaque receiver, an import the index could not place) — a candidate use to verify by reading the file (--no-include-unresolveddrops these); - possibly a
(via_alias)marker: a supplementary row for the alias/re-export hop the primary reference went through.
Check the stderr coverage note before trusting absence. It reports when the
symbol index for this ref is not built, skipped (ref too large to resolve),
partial (some files failed to parse), or at another commit than the ref's
indexed head — in every one of those, a missing symbol may simply be
unindexed. Absence is not completeness.
A stale exact target (definition renamed/removed since the defs call) errors
and tells you to re-run ccx defs for a current target.
Resolved rows alone do not settle "no callers". A use the resolver could
not commit — an opaque receiver, an import across a Python source root the
index could not infer — appears as a ~name row, never as a resolved one: read
those rows (--role call narrows them) as candidate uses and confirm by
opening the file; re-run without --no-include-unresolved if you dropped them.
A literal No references. with no coverage note (the Using git ref line is
not one) is strong evidence, but where refs cannot see at all — string keys, dynamic dispatch, an uncovered
language — ccx grep the call or registration shape before calling it settled.
Settling what runs. To confirm a candidate is the live path, look at its
uses, not its text: ccx refs <candidate> lists them, and reading one gives the
verdict. A plain call on the path confirms the candidate. x or candidate()
means the path's value comes from x, so the candidate is only a fallback — the
answer is upstream, in whatever supplies x; do not report the fallback as the
decider. An entry in a registry or dispatch table means configuration picks, so
name the key that selects it. Where refs can't see (string keys, dynamic
dispatch, an uncovered language), ccx grep the call or registration shape
instead; and when the question names an entry point, ccx defs <entry> and
walking down reaches the live code without ever weighing a look-alike hit.
Ask a question, get a cited answer (ccx ask)
Ask first when the deliverable is an explanation rather than a location:
one ccx ask call replaces a chain of searches. Every command above returns
material — hits, matches, symbol rows — for you to read; ccx ask returns a
written answer with citations, because a server-side agent runs that chain
for you and writes up what it found. Reach for it on "how does X work end to
end?", "why does Y happen?", "walk me through the release flow", "compare A
and B" (above all across repos), and on any question too broad to reduce to
one search or pattern. When a question wants both — an explanation and every
call site — ask for the explanation, then settle "every" with defs → refs.
Scoping is the same as search: the current checkout by default, --repo
repeatable, --git-ref for a single repo.
ccx ask "how does the indexer decide what to re-embed?"
ccx ask "compare how these two services authenticate" --repo acme/a --repo acme/b --effort high
ccx ask --parent 7e3a0012 "and where is that retried?"
Follow up instead of re-asking. When an answer printed a follow-up:
command on stderr and your next question builds on it — a detail, the next
step, or the reply to an answer headed ## Clarification needed — run that
command with your question (ccx ask --parent <answer-id> "<follow-up>"):
the evidence is already in context, so the investigation is not repeated, and it needs no --repo (the
scope is the answer's). parent_outdated prints the conversation's questions
in order: ask the first again, then re-issue the rest from each new id.
Pick the effort level from the question's shape with --effort: low
for a lookup with a known shape, nothing (the server default, usually
medium) for a how-does-this-work question, high for architecture,
comparison, and multi-repo questions. Higher levels let the agent take more
steps and run longer; they cost more and are worth it only when the question
needs the breadth.
What it costs: ask runs seconds to minutes, where search, grep, defs
and refs answer in a second or two — so when the deliverable is a location
or code to read (which file to change, where a symbol lives, its call
sites), stay with those. Run one ask at a time (the server caps concurrent
agentic requests), and do not wrap it in a shell timeout — the server
enforces its own deadline (ten minutes by default, twenty at --effort high);
if your own command runner has a shorter limit, raise it or run the call in the
background.
- Read it as evidence, not proof. Citations look like
[s0:path#L40-L52];s0resolves on stderr ass0: <owner>/<repo> @ <ref> (commit <sha>), and the line numbers are at that commit. Before acting on a claim, open those lines at that commit —git show <sha>:<path>, or your working tree when it is at that commit; for a repo you don't have,ccx read-filewith that--repo/--git-ref(the ref's indexed head — the same commit unless the index has moved since). Drop a claim its lines don't support; don't repeat it. - A forced answer may be incomplete. When the agent runs out of steps or
of room first, stderr says
this answer was forcedand the answer ends with a## Not verifiedsection. Treat those items as open: when the notice ends with accx ask --parent …command, run it; otherwise check them withsearch/grep/defs/refs, or ask once more with--effort high(or a narrower question) — never repeat the same ask at the same level. - It may be off, busy, or out of time.
agent_query_unavailablemeans the deployment hasn't enabled it: answer with the other commands, tell the user their platform team decides, and don't try again this session — unless it answered a--parentand names the answer cache: then ask the question afresh without--parent, or retry shortly when it says so.agent_query_busy(concurrency cap) and anHTTP 503Request deadline exceeded(the investigation outran the server's deadline): fall back to the other commands for this question — at most one later retry, narrower or at--effort highfor the longer deadline, never a loop.
Reading & listing files at a ref (remote / cross-ref)
ccx read-file and ccx find-files fetch file contents and paths at an indexed
ref. They're for code you don't have on disk — a repo not checked out locally,
or a different ref than your working tree. When the repo is checked out at
the ref you care about, use your normal file tools instead (faster, same bytes).
Full usage: references/remote-access.md.
Repo & ref metadata
ccx repos lists the indexed repos you can target (alias, stable uid, default
branch, and the refs each is indexed at as <ref>@<commit>).
ccx git-refs [<owner>/<repo>] lists one repo's indexed refs with their full
commit shas ((default) marks the default branch) — reach for either to target
a non-default ref, or to see which commit a ref is indexed at, not as a routine
pre-flight; the ref default already handles the common case.
Note the two different senses of "ref": ccx git-refs lists git refs
(branches and tags), while ccx refs finds where a symbol is used.
When ccx itself fails
Errors exit non-zero; notes (the ref used, subtree scoping, symbol coverage) go
to stderr and results to stdout — read both. Server and environment failures —
HTTP 503 (index tables not built yet), HTTP 401, an expired login (the
error says Run ccx login), a connection error, or ccx not installed — are
the user's environment, not something to work around: never invent a server
URL or token; tell the user which piece is missing.
references/management.md has the symptom table,
and the mapping to the server's MCP tools for agents that have those instead
(ccx ask is ask_codebase).