Start

CLI reference

cg <command> is python -m cg_code_graph.cli <command>. Query commands need --db. Flags not listed here: cg <command> -h.

Commands at a glance

CommandOne lineDetail
index ROOT --db DBBuild the graph. Stats JSON on stdout; coverage on stderr. --name, --gates, --scip (repeatable). Missing toolchain skips that language. Invalid .cg.yaml exits 2.configuration
detect ROOTLanguages and frameworks under ROOT, no index.cg detect -h
doctor [ROOT]Versions, extractors, and exact / heuristic / unavailable per language. ROOT limits this to the project. --scip FILE checks an index (repeatable). A module that fails to import exits 1. Says when a newer release is on PyPI (one request with a 2 s timeout, cached for a day; silent offline; off with CG_NO_UPDATE_CHECK=1 and under CI). cg --version repeats that notice from the cache and never connects.install
setup [LANG]...Install TypeScript / PHP / Dart extractors now (--quiet). Default: every language whose toolchain is present.install
setup --pruneRemove extractor installs this cg does not use. --dry-run lists them. A held lock is kept.clean
config show|validate [ROOT]Effective config and where each value comes from, or check .cg.yaml (exit 2 if invalid). Same flags as index, routes, and serve (--no-apps included).configuration
coverage --db DBOne-line summary per repo and per language that is not fully indexed. Every non-zero bucket is on that line, excluded included, and indexed plus the buckets equals discovered. A TypeScript-plugin entry with no .ts / .tsx / .mts / .cts files is labelled javascript (JSON keeps language and adds label). --details names each excluded file with why (tool config, test file, .cg.yaml exclude). --all-files lists every path.completeness
starters --db DBStarter queries, each with the matching command and MCP call.viz
link --backend DB --frontend DB --db OUTMerge graphs and match client HTTP calls to routes. Two repos: --backend / --frontend. N repos: repeat --repo NAME=DB[:role]. --report PREFIX writes .json and .md.workspace
reaches SPEC...Dependents, grouped by entry classification. On a base method, overrides are followed. A table target also follows its columns. --connection NAME keeps one database connection (connections).specs
impact SPECCallers up to entry points. --plans-dir adds external snapshot clients. A connections: line lists the non-default database connections the function's own table / column edges go to (connections).answer shape
downstream SPECForward walk: page, composables, HTTP, routes, services, tables.specs
path SRC DSTOne shortest evidence chain. No path: reason on stdout, exit 1.answer shape
routesGuards, what the route reaches, and frontend callers on a linked graph.routes and guards
search NAMENodes by name or FQN substring, plus middleware / guard / auth names. --kind, --limit.cg search -h
snippet SPECSource from the index: path:start-end, then numbered lines. --context, --max-lines (default 200). Ambiguous name lists candidates and exits nonzero; that reply and a miss end with a cg search <name> hint (the MCP snippet tool says the same).specs
explore QUERYQuestion or spec → source, entry points, call paths, blast radius in one answer. --budget, --json.explore
node SPECLocation, fqn, platforms, attrs, and edges (one site, with a count).cg node -h
statsProject, languages, coverage line, node and edge counts.cg stats -h
writers SPECWriters of a table, a column, or a stored property (WRITES_TABLE, WRITES_COLUMN, WRITES_PROP). SPEC is table, table:X, table.column, column:table.column, Type.prop or Class::$prop. A column lists its column writers, then "table-level writes (columns not recorded)". --json rows carry target_kind (table, column, property) and connection. --connection NAME keeps one database connection.spec forms
readers SPECReaders of a table (READS_TABLE, READS_COLUMN grouped by column), a column (READS_COLUMN), or a stored property (READS_PROP). Same SPEC forms as writers. Heuristic string matches (MENTIONS_COLUMN) come last and are labelled. --connection NAME keeps one database connection.spec forms
siblings SYMBOLHierarchy, the same method on siblings, shared resources, co-callers.cg siblings -h
roundtrip Type.propHeuristic lossy-write / UI-seed pairs. --tests includes test code. Nothing is written to the graph.heuristics
lint async-stateHeuristic rules stale-async-result, two-writers, incomplete-cache-key, echo-suppression. --rules is a subset. --tests includes test code.heuristics
api-calls SPECClient endpoints, call sites, request keys, matched route. SPEC is all, unmatched, a substring, or a * glob.cg api-calls -h
channels [PATTERN]Who can join, who publishes, who listens. --no-source hides source.channels
bridges [PATTERN]Web / native bridges and Electron / Tauri boundaries. --protocol (capacitor, react-native, flutter, flutter-event, electron-ipc, electron-preload, tauri), --unmatched.bridges
protocols [PATTERN]Summary per protocol, or one endpoint's senders, receivers, guards, and checks (no_receiver, no_sender, ambiguous, schema_mismatch, unguarded). --side send|receive, --listeners (TCP / UDP binds).protocols
surfaceSecurity views: findings (hardcoded, plaintext, unverified, unguarded, exposed-listener, tls-off, ipc-exposed, ssrf) and the inbound / outbound surface. --inbound, --outbound, --protocol, --finding, --min-confidence, --format text|json|sarif, --fail-on (exit 1), --strict, --show-ignored.surface
tools [PATTERN]LLM and MCP tools. --framework (mcp, openai, anthropic, langchain, openai-agents, llamaindex, custom), --agent, --unmatched (no_receiver, no_sender, name_collision).ai-tools
external [PATTERN]External systems, who uses them, address and credential source, TLS. --protocol (postgres, mysql, redis, smtp, amqp, mongodb, ldap, ssh, ftp, s3, https, saas, aws, gcp, azure, k8s, docker, …). --source is literal, env, env-example, compose, or config. --tls-off.external
tests SPECDirect and transitive tests. --unit-only, --exclude-root (repeatable), --through-roots. A file or module spec covers every symbol in it.channels
affected [FILES...]Tests and entry points a change reaches. --base REF uses only the touched lines; --quiet prints test files.affected
parity --db SRC --against TGTSymbols in SRC with no counterpart in TGT. --map, --strip-prefix (repeatable), --no-fuzzy. --structure also pairs by use; --no-learn and --write-map apply only with it.parity
platforms [summary|divergence]Targets and tagged symbols, or divergence findings. --target. --kind is variants, api_surface, missing_callee, or missing_callee_tests.platforms
resolutions CONCEPTWhere a value is resolved, its fallbacks, and whether the client sends it. --within, --no-client.value facts
plan ACTION [NAME]list, load, validate, check, baseline. --verify, --summary, -o.plans
serve --db DBLocal web UI. --port, --host, --plans-dir, --presets.viz
viz-export MODE SPECS -o OUTSelf-contained HTML. MODE is reaches, impact, downstream, or path. --sinks.viz
viz-plan NAME -o OUTSelf-contained HTML of a plan on the graph. --plans-dir.plans
agents ACTIONOpt-in block in agent guidance files. Previews and asks. --mcp also writes the cg-mcp server entry.mcp
install / uninstallRegister or remove the cg MCP server (key cg) in a host config. --host, --global, --dry-run.install
hooks install|uninstall|statusOpt-in git hooks that refresh the index after commit, checkout and merge. Nothing is installed by default.hooks
refresh ROOT --db DBRe-index when sources changed. --name, --quiet. Hooks run this in the background.hooks
watch ROOT --db DBRe-index on save, debounced (watchfiles if installed, else polling). Ctrl-C stops.watch
clean [ROOT]Cache entries for one project, --stale ones, or --all.clean

Common flags

FlagWhereMeaning
--db DBevery query, plus index, link, serve, vizgraph file
--jsonmost commandsone JSON document (config show: the effective config)
--min-confidence {heuristic,resolved,exact}reaches, impact, downstream, path, writers, readers, siblings, node, stats, api-calls, routes, tests, viz-exportdrop edges below this
--max-depth Nreaches, impact, downstream, writers, readers, siblings, node, stats, api-calls, testshop cap (tests: 0 is any depth)
--no-pathsthe --max-depth commands, plus routesomit evidence chains
--gate GATEthe --max-depth commands except testslive/gated split; default the scenario that was indexed (auto); none disables
--connection NAMEreaches, writers, readersonly table / column edges on that database connection (attrs.connection; a dynamic name by its pattern, legacy_{store.id}). connections
--platform TARGETreaches, impact, downstream, path, routes, searchone build: windows, linux, macos, ios, android, web. The first line names the filter and how many conditions could not be evaluated. platforms
--max-items Nlist commands (routes, protocols, tools, external, parity, platforms, plan, …)cap listed rows

node --json is a list of {node, out, in}. stats --json is {project, root, indexed_at, nodes, edges, nodes_by_kind, edges_by_kind, stats}.

Workspace

--backend and --frontend merge two graphs. --repo (repeatable) merges any number. The two forms cannot be combined. role is backend, frontend or both (default both).

cg link --backend out/api.db --frontend out/web.db --db out/graph.db
cg link --repo orders=out/orders.db:backend \
        --repo gateway=out/gateway.db:backend \
        --repo web=out/web.db:frontend --db out/graph.db

Every repo's client calls are matched to routes on every other server, so a backend may call another backend. A frontend links: list limits which servers that app calls (apps and workspace). Channels, protocols, payload checks and entry tagging run once on the combined graph. Stats include one row per client/server pair plus totals.

Webhook sections of the --report markdown (#152 part B):

  • ## Webhook subscriptions paired by URL: each subscriber URL a sender repo records (seed data, config, .env.example) whose path matches a route of another repo (heuristic MATCHES_ROUTE from the sender endpoint), or unpaired: <url> (<reason>).
  • ## Routes called back by external parties: routes that a REGISTERS_CALLBACK edge names, with the registering function, the body key and the outbound endpoint. These routes are left out of ## Backend routes without a client call, as are routes a subscriber URL pairs with. The registering repo needs the role backend or both, because routes of a frontend repo are not read.
## Webhook subscriptions paired by URL

- `route:POST /webhooks/orders` <- subscriber URL https://bookstore-api.test/webhooks/orders (heuristic) from `endpoint:webhook:sender-php:order.paid`; configured at sender-php/database/seeders/WebhookSubscriptionSeeder.php:13
- unpaired: https://elsewhere.test/hooks/none (not a backend URL (no route shares its first segment: local asset / device bridge / external)) from `endpoint:webhook:sender-php:order.paid`; configured at sender-php/database/seeders/WebhookSubscriptionSeeder.php:18

## Routes called back by external parties

- `route:POST /webhooks/payments/{store}` <- callback URL registered in `PaymentsClient::createPayment` (body key `callback_url`, outbound `http:POST https://pay.provider.test/v1/payments`) at sender-php/app/Services/PaymentsClient.php:15

An id that exists in two repos is stored as repo: plus the original id (orders:class:Order), and edges that used it are rewritten. external: and endpoint: ids stay a single node. Every other id is unchanged, so a two-repo link with no shared ids keeps the same ids. orders:Order and orders:class:Order select that repo; a plain Order matches every repo that defines it (specs).

cg index on a workspace .cg.yaml writes one combined --db for every app (apps and workspace).

Query targets (specs)

FormSelects
table.column, column:table.columna DB column
table:NAMEa DB table
connection:NAME, connection:tenant_*a connection from config/database.php, plus dynamic Config::set('database.connections.…')
env:KEY, config:dotted.keyan env or config key
Class.method, Class::method, Class, FQNa symbol, suffix match
repo:Class.method, repo:class:Orderthat symbol in one repo of a combined graph (orders:Order.total). A plain name matches every repo
page:/reports/:ida Nuxt page by route
app/pages/x.vue, app/composables/useX.tsa TS module or Vue SFC (repo-relative suffix)
useX, useX.fn, fna TS composable, store, or function
src/app.ts#listOrders, svc.ts#OrderService.createa TS / JS / Vue symbol in one file (file#name~2 is the second hit; function:src/app.ts#listOrders is the full id)
http:GET /v1/…, route:GET /v1/…a client endpoint, or one backend route. route:VERB /path is exact: the verb is case-insensitive, and :id / {id} / {order} match the same {param} segment. route:POST /payments/* (* spans segments) and route:* /payments are the wider forms. An unknown route exits 1 with no such route and up to five nearest routes
channel:orders.{order}, channel_sub:orders.{id}a broadcast channel or a client subscription
test:tests/Feature/OrderTest.php::…a test case
request_key:timezone, setting:reports.timezonea value fact (value facts)
crate::mod::Type::method, mod:path, feature:, cfg:, define:, unsafe:Rust / C / C++ (native)
  • Either separator works. A spec that misses is retried with the other. Swift, Kotlin, Python, TS, and Dart fqns use .; PHP, Rust, and C++ use ::.
  • Sub.method for a method Sub inherits and does not redefine resolves to that definition (B.run -> inherited from Base.run; --json has fqns). Callers are the definition's, narrowed by receiver type (attrs.recv): a receiver that cannot be a Sub is left out and counted (callers narrowed to B: 2 of 6 calls on other classes left out). Unknown receivers, and ancestry the graph does not know (extends mix(Base)), stay. reaches and tests narrow the same way. Only overrides in Sub and its subclasses are followed.
  • When Sub overrides the method, callers of the base (via base) drop calls whose receiver cannot be a Sub (override_narrowed in JSON).
  • Several specs in one reaches call are unioned.

Answer shape

  • index leaves generated, copied, and vendored files out unless --include-generated (labelled attrs.generated; generated). --python-root (repeatable) replaces detection and python.source_roots. Workspace apps are indexed into one combined graph unless --no-apps (apps and workspace). --scip is repeatable. Starters that miss a 20s budget are named in starters_skipped.
  • impact marks a held function (ref: collection | callback | assignment | decorator) and a dispatch or plugin list (call through a collection). Override lines are overrides: Base.m and overridden by: A.m, B.m. The base is not a caller of its override; a call of the base declaration is (via base Base.m). impact on a base also lists callers of the overrides, (via override A.m +1).
  • path to table: with no table edge ends at the column that is read or written. A hop that passes keys the next request never sends adds note: sent but not forwarded: ….
  • api-calls folds a runtime or env base URL ((base {runtimeConfig.apiBase} = …)). Endpoints only tests call are marked (called from tests only). A call site whose function no entry point reaches is (no live caller) (or (no live caller found; N files not indexed) when TypeScript files or unresolved Vue navigation could hide a caller). A wrapper exported from package.json main / exports is (exported). The summary adds , N with no live caller. cg api-calls dead lists matched endpoints with no live call site. --json has live on the endpoint and each call site. Each call site lists body keys: and query keys: when the extractor recorded them (a conditional key ends with ?). A body sent as axios data (delete, request, ...) or $fetch body is listed too; params are the query keys. --json includes body_keys and query_keys on the call. A route: spec filters on the matched route only (exact, unless the spec contains *). An unknown route prints no such route: VERB /path, up to five nearest routes, and with --json {"error": "no_such_route", "spec", "suggestions"}.

Readers and writers specs

cg writers SPEC and cg readers SPEC use one resolver. A spec that names both a stored property and a column (a Python class field and a table with the same dotted name) lists both groups, each labelled.

SPECwritersreaders
books, table:booksWRITES_TABLE and WRITES_COLUMN of every columnREADS_TABLE, then READS_COLUMN per column, then MENTIONS_COLUMN
books.title, column:books.titleWRITES_COLUMN into the column, then table-level writes of books (columns not recorded): WRITES_TABLE edges from functions that record no column write to that tableREADS_COLUMN, then MENTIONS_COLUMN (heuristic string-literal matches, labelled)
Cart.total, Cart::$total, field:Cart.totalWRITES_PROPREADS_PROP
$ cg writers books.age_rating --db out/api.db
[Http/Controllers/Admin] App\Http\Controllers\Admin\BookController::update  WRITES_COLUMN column:books.age_rating  @app/Http/Controllers/Admin/BookController.php:30 (resolved)  entries: http_route
table-level writes of books (columns not recorded):
  [Console/Commands] App\Console\Commands\SyncWarehouseCommand::handle  WRITES_TABLE table:books  @app/Console/Commands/SyncWarehouseCommand.php:19 (resolved)  entries: artisan_command
2 write edges, 2 writers

The table-level group omits a function that already has a column write to that table. Entry kinds are listed per site (entries: http_route, scheduled, queue_job, artisan_command, ...). To answer "does anything write this table in the background?", check entries: for scheduled or queue_job. No such entry means the indexed code declares no scheduled or queued writer, not that none runs in production (whether a scheduler or worker is running is out of scope). --json is the flat list of rows; each row has target_kind (table, column or property) and group (table, column, table_level, mentions, property). The MCP tools writers and readers take the same specs and print the same groups. cg reaches prints the same entries: on each writer.

Database connections

Table and column edges record the database connection they go to (attrs.connection; PHP, schema). Output shows it only when it is not the default:

  • reaches, routes --reaches and path add conn=<name> to the edge step: READS_COLUMN[resolved @ app/Services/ArchiveService.php:13 conn=legacy_{store.id}]-> column:books.store_id, and READS_COLUMN@ArchiveService.php:13~r conn=legacy_{store.id} in a route line.
  • writers and readers add conn=<name> to the row; --json rows carry connection (and connection_default) on every row.
  • impact on a function prints connections: <name> (<how>, <file>:<line>; <Model> default: <connection>) for each non-default connection its own table and column edges use, and nothing when it uses only the default.
  • --connection NAME on reaches, writers and readers filters by that name. With no match the reply names what exists:
$ cg writers books --connection warehouse --db out/api.db
no writers recorded for table 'books' on connection 'warehouse' (connections seen: mysql 26 edges, legacy_{store.id} 1 edge).

An edge whose connection could not be resolved is ?. The MCP tools reaches, writers and readers take connection; reaches, impact, writers and readers print the same labels.

Empty results

impact, writers, readers, siblings, path, routes, search, and resolutions say why a miss happened and name a next query: a missing table lists similar names, an unknown column lists the table's columns and close matches (table books has no column 'titel'; similar: title. columns: id, store_id, ...), a spec that matches nothing says what was tried (no stored property, table or column 'X'; the Swift / Kotlin field: hint appears only when the graph has those languages), a method with no callers lists other incoming edges, a symbol with no siblings points at callees that touch data, and a missing path reports the opposite direction when one exists.

$ cg siblings StockService::reserve --db out/graph.db
target: Services\StockService::reserve
no siblings found for Services\StockService::reserve: its class has no parent class, interface or trait shared with other classes; it touches no table, column, config, env key or connection directly; all 2 of its callees are in its own class, which co-caller matching skips.
try: siblings('Services\StockService::reserveLocal') (a callee that touches data); siblings('Services\StockService::reserveFromWarehouse') (a callee that touches data); impact('Services\StockService::reserve') for its callers and entry points; downstream('Services\StockService::reserve') for the tables, config and connections it reaches

Partial answers

routes, impact, reaches, and tests mark a partial answer: a route or handler registration cg does not model, or a file in the answer's language that is not indexed. Totals then read as indexed totals. A coverage note: names where to search by hand. On a combined graph each file-count or heuristic fragment names its repo: 3 TypeScript / JavaScript files not indexed (bookstore-api), 3 TypeScript / JavaScript files not indexed (bookstore-web). --json carries the same facts in completeness. A complete answer keeps the usual wording. completeness

$ cg routes --db out/shop.db
all routes: 1 indexed (possibly more: 1 unmodelled route registration)
coverage note: 1 route registration cg does not model (Django urlpatterns built by a function call, comprehension or loop: shop/urls.py:9). There, use your normal search and file reading (an empty cg answer is not proof of absence).

Routes and guards

routes joins guards, what the route reaches, and who calls it. One evidence chain per route. On a combined graph, frontend callers are included.

  • Guards come from Laravel route and group middleware, Nest @UseGuards / @UseInterceptors / @UsePipes (plus APP_GUARD and global guards / interceptors), Express / Fastify / Koa / Hono / Elysia, Next.js middleware.ts, django-ninja auth=, Django view decorators and access mixins, and DRF permission_classes / authentication_classes.
  • Scope. --writes or --writes TABLE, and/or --reaches SPEC.... No scope lists every route.
  • Auth, in order: --auth-pattern / .cg.yaml auth.extra_patterns, the preset's auth guards (not csrf_protect, ThrottlerGuard, AllowAny, …), then the auth name pattern. Each guard records auth_by. Presets: configuration.
  • Laravel kernel middleware and Django MIDDLEWARE apply to every route and are not repeated.
  • External callbacks. A route whose URL the app registers with an external party (REGISTERS_CALLBACK) shows client: external callback (PaymentsClient::createPayment); --json carries them as callbacks. Webhooks
  • Filters. --unguarded keeps routes with no auth guard. An unconditional inline check counts, so those routes are left out. --unguarded --strict counts route guards only. --missing NAME keeps routes whose route guards and inline checks do not contain NAME.
  • Inline guards (Laravel). Checks the handler runs before its own work are a separate line, not route middleware. --json adds inline_guards: name, kind (policy, permission, secret, role), at, conditional. auth.extra_patterns selects project method names when that method rejects, or when the caller aborts on its result. A conditional check stays listed and does not hide the route from --unguarded. See PHP.
  • search matches the same guard names, including inline checks.
$ cg routes --db api.db
POST /v1/orders/{order}/refund  @routes/api.php:25
    guards: auth:sanctum [auth]
    inline: hasPermission('orders.refund') [permission] RefundController::store
POST /v1/warehouse/sync  @routes/api.php:26
    guards: (none)
    inline: ensureValidSecret (hash_equals config:bookstore.sync_secret) [secret] WarehouseSyncController::ensureValidSecret

--unguarded lists neither route. --unguarded --strict lists POST /v1/warehouse/sync (no route middleware; the secret check is inline).

$ cg routes --writes books --missing auth:api --db out/graph.db
routes reaching a write to books: 3 of 9 routes | filter: missing a guard matching 'auth:api' -> 2
auth guard: 0 with, 2 without (auth = a framework preset auth guard or a name matching the auth pattern)

POST /v1/admin/books  @bookstore-api/routes/api.php:23  NO AUTH
    guards: (none)
    writes books via Http\Controllers\Admin\BookController::store conf=resolved  ROUTES_TO@api.php:23 → WRITES_COLUMN@BookController.php:15~r → column:books.store_id

PUT /v1/admin/books/{id}  @bookstore-api/routes/api.php:24  NO AUTH
    guards: (none)
    writes books via Http\Controllers\Admin\BookController::update conf=resolved  ROUTES_TO@api.php:24 → WRITES_COLUMN@BookController.php:30~r → column:books.title

Heuristics

Both are labelled heuristic in text and in JSON (confidence). Neither adds edges.

  • roundtrip pairs a write of a stored property through a lossy transform with a read that seeds UI state (path: write, lossy call, property, read, seed, each with file:line). MCP tool: roundtrip(prop).
  • lint async-state runs all four rules unless --rules lists a subset. MCP tool: lint_async_state.

explore

cg explore answers a question in one call: matching symbols, then entry points, call paths, blast radius and source. The same answer is the MCP tool explore (MCP server).

Resolution, in order:

  • A spec that matches 1 to --max-symbols nodes wins (a node id, catalog.api.place_order, or POST /api/orders/). A bare word that only names a test fixture or a module falls through to the word match.
  • A token that is a source file (catalog/api.py) uses the symbols defined in that file.
  • Otherwise the words are stemmed and matched. module, file, field, column, constant and test nodes (tests, and code in test files such as conftest.py) are left out. A word in a symbol's own name counts more than one found only in its module path; the highest scores are kept.

Sections:

  • entry points — up to six per symbol, with kind, name and confidence (exact, resolved, heuristic).
  • call paths — the path from the first entry point, then a path between resolved symbols. A hop is -KIND[confidence @ file:line]->.
  • blast radius — callers, tables written and read, env keys, jobs and connections, each with a confidence label.
  • source — numbered lines, grouped by file, inside the token budget. A cut-off body ends with a snippet(...) hint; symbols that did not fit are listed the same way.
  • next — the fine-grained calls to run after this answer.

The default budget is 3000 tokens (about 4 characters each), clamped to 500..20000. Header, entry points, call paths, blast radius and next steps come first; when they alone would not fit, the lowest-ranked symbols are dropped and listed under next as explore(...) calls. Source fills what remains. The last line is budget: used/limit tokens. Nothing matched exits 1.

flag
--budget Ntoken budget (default 3000, clamped to 500..20000)
--max-symbols Nsymbols to expand (default 4)
--jsonprint the result object
--db DBgraph database (required)

affected

cg affected maps a change to the symbols it touches, then to the tests and entry points that reach those symbols. A path argument is the whole file. --base REF (alias --git-diff) reads git diff against that revision and keeps only the touched lines. The innermost symbol wins: a changed method, not the class that contains it. A pure rename uses both the new path and the old one; a rename the index has not caught up with falls back to the old path. A deletion is resolved from the index. A changed test is listed itself (changed). A file with no symbols is reported as not in the index.

The same expansion is what a file or module spec does in cg tests (app/util.py, app.util): every symbol defined in that file. See Broadcast channels and tests.

--quiet prints the test files, one per line, so a runner can take them:

pytest $(cg affected --base main --db out/graph.db --quiet)

At most --max-targets symbols are walked (default 200, 0 for no cap). The text report says when it stopped early and JSON reports it under truncated. With no test found, --quiet prints nothing, and a bare pytest would then run the whole suite. No files, no --base and no --stdin exits 2. A bad revision, or --base outside a git checkout, exits 2. Finding no tests is still exit 0.

flag
--base REF, --git-diff REFonly the lines the diff touches
--stdinone path per line on stdin
--root DIRgit root (default: the indexed root)
--quiettest file paths only
--max-depth Ntransitive tests within N application hops (0: any)
--unit-onlyleave out UI / snapshot tests
--max-targets Ncap on symbols walked (0: none)
--jsonthe result object, plus completeness
--min-confidenceheuristic (default), resolved, exact

agents

install and update write one block between <!-- BEGIN cg agent rules … --> and <!-- END cg agent rules -->. Bytes outside it stay. remove deletes the block. show and --dry-run print the diff only. Otherwise cg prints the diff and asks apply N change(s)? [y/N]; --yes skips that. A second run replaces the block in place; when nothing would change it reports no change (block up to date) and writes nothing.

The first selected file in the table order (AGENTS.md, else CLAUDE.md, else the Cursor rule) gets the full block. The others get a one-line pointer inside the same markers. The block pins pip install 'cg-code-graph>=MAJOR.MINOR' (the published package). Re-running converts an old full copy into a pointer when that file is no longer primary.

--targetfile
agentsAGENTS.md
claudeCLAUDE.md
cursor.cursor/rules/cg.mdc

--target repeats. --all selects all three. With neither, cg acts on the files that already exist, and exits 2 when none exist unless --mcp was given. --dir is the project root (default .).

--mcp adds, or on remove deletes, one cg entry under mcpServers: {"command": "cg-mcp", "args": ["--db", "out/graph.db"]}. Default path <dir>/.cursor/mcp.json (--mcp-file overrides). The entry is edited in place; other keys stay. Invalid JSON is left untouched. mcp

install

cg install registers the MCP server under the key cg. cg uninstall removes it (and a cg-owned legacy code-graph entry). Both preview a diff and ask apply N change(s)? [y/N] unless --yes. --dry-run prints the diff and writes nothing.

flagmeaning
--host Hcursor, claude, claude-desktop, vscode, windsurf, codex, gemini, zed, or all. Repeatable. Default: hosts already detected for the scope
--projectproject config (default)
--globaluser config. Requires --db (a global entry serves one graph), unless --registry
--dir ROOTproject root (default .)
--db DBgraph file. Project default: <dir>/out/graph.db. Not used with --registry
--registryone global cg entry and no --db; the server reads the graph registry
--migratefold per-repo cg-* entries into the registry. Dry run unless --apply
--applywith --migrate, write the registry and the host config (a .bak of each)
--graphs FILEregistry path for --migrate (default ~/.config/cg/graphs.toml)
--tools LISTallowlist stored as --tools (Choosing tools)
--portablecommand cg-mcp and --db out/graph.db, for a file committed to git
--uninstallremove, on cg install
--no-rulesskip the agent rules. By default, project scope also writes a marked <!-- cg:start --> section in AGENTS.md and an alwaysApply .cursor/rules/cg.mdc telling agents to use cg first. A re-run replaces only that section, and cg uninstall removes it (Install into a host)
--dry-runprint the diff and write nothing
--yesskip the prompt

Exit 0 on success or when nothing changes. Exit 2 on invalid JSON or TOML (the file is left untouched), a refused host, --global without --db and without --registry, or when no host is detected. Exit 1 when the prompt is declined. --migrate without --apply only prints the plan.

cg uninstall takes the same flags. See Install into a host.

hooks

install, uninstall and status manage one block in post-commit, post-checkout and post-merge. Nothing is installed by default. The block sits between # >>> cg hooks (managed by cg hooks; edit with cg, not here) >>> and # <<< cg hooks <<<. cg finds that directory with git rev-parse --git-path hooks, so core.hooksPath and worktrees apply. Linked worktrees share one hooks directory, and it holds one block, so the last install decides which root and --db it refreshes. A core.hooksPath outside the repository (shared by other repositories) gets a note at install time. Not a git repository: exit 2.

A missing hook becomes #!/bin/sh plus the block, mode 0755. An existing shell hook keeps every other byte; the block is inserted immediately after the shebang, so a later exit 0 cannot skip it. A second install replaces that block in place. A hook with no shebang, or a non-shell shebang, is left unchanged and reported as skipped (not a shell hook). uninstall removes only the block and restores the original bytes. A file that then contains only the #!/bin/sh cg wrote is deleted.

The block never calls exit. It backgrounds the refresh, discards its output, and ends with a succeeding command, so git is not blocked and a hook failure cannot fail the git command. post-checkout does nothing when $1 equals $2 (HEAD unchanged). CG_NO_HOOKS=1 skips the block (Configuration). The command is the absolute cg next to the Python that ran cg hooks install, when that file exists; otherwise '<python>' -m cg_code_graph.cli refresh '<root>' --db '<db>' --quiet; otherwise cg refresh … when cg is on PATH. If none of those exists, the block does nothing. A block that still runs -m codegraph.cli, or that still checks the pre-0.19 hook switch, is outdated: cg hooks status and cg doctor say so, and install rewrites it. Run install again after moving or recreating that environment.

cg refresh ROOT --db DB re-indexes when the checkout changed. Every run touches <db>.refresh.pending, then takes <db>.refresh.lock (non-blocking flock, or msvcrt.locking on Windows; the OS releases either if the process dies). If another refresh holds the lock, it prints refresh already running; marked pending and exits 0. The holder clears the flag before each pass and runs another pass while it is set again, at most three passes per run, so a burst of checkouts collapses into a few runs and a request that arrives mid-pass is not lost. When the database exists and ROOT is a git repo, cg hashes the path, mtime and size of every file from git ls-files -c -o --exclude-standard; if that matches <db>.refresh.state (written by the last successful refresh, together with the database's mtime), it prints up to date and exits 0. A rename, an added or deleted file, an edit, or a database rewritten by something else (cg index) re-indexes. Not a git repo, or a workspace app root outside ROOT: always re-index. Otherwise it indexes into <db>.refresh.tmp (root, name and recorded --python-root / --include-generated flags from the database, or the CLI arguments when the database is new), refuses a 0-node result without replacing the database, then moves the file into place with an atomic rename. A workspace apps: list is indexed on that temporary path as well; per-app databases derived from it are moved next to the real --db. A combined graph from cg link (no apps:) is refused and left unchanged. Git's own GIT_DIR / GIT_INDEX_FILE and similar variables, which git sets for hooks, are ignored, so ROOT is always the repository indexed. The last run is written to <db>.refresh.log. Exit 0 means refreshed, up to date, or another refresh holds the lock; a failure or refusal is nonzero, and the hooks ignore it.

FlagMeaning
--dirproject root for hooks (default .)
--dbgraph file; required for install; stored absolute. Required for refresh
--dry-runprint the diff and write nothing
--yesskip apply N change(s)? [y/N]
--quietrefresh only: no stdout
--namerefresh only: project name when the database is new

The database file and its sidecars (<db>, <db>.* and <db>-*, including -wal and .refresh.*) are left out of the fingerprint, so a database that lives inside ROOT and is not gitignored still reports up to date on the next run.

watch

cg watch ROOT --db DB runs the same refresh as the git hooks, once at start and again when sources change. Install the optional backend with pip install "cg-code-graph[watch]". Without it, cg polls the git fingerprint (--poll forces that path). Polling needs a git repository and exits 2 otherwise.

.gitignore applies because a refresh runs only when the git ls-files fingerprint changes. watchfiles also skips vendor, build, dist, target, out, .next, .nuxt and .dart_tool, and both backends ignore the database and its sidecars. Polling waits until two checks --debounce seconds apart see the same fingerprint (at most 30s) and then refreshes once. Before each refresh, cg waits while git rev-parse --git-path index.lock exists (a branch switch), so a checkout is one refresh. If files change while a refresh runs, one more refresh follows, then cg waits for the next change. The run shares <db>.refresh.lock with cg hooks. Ctrl-C prints watch stopped and exits 0.

FlagMeaning
--dbgraph file (required)
--nameproject name when the database is new
--debounceseconds to let writes settle (default 1)
--intervalpolling interval in seconds (default 2, minimum 0.2)
--pollpoll with git instead of watchfiles
--quietpass --quiet through to each refresh

clean

Cache root: $CG_CACHE, else %LOCALAPPDATA%\cg, else $XDG_CACHE_HOME/cg, else ~/.cache/cg. A directory left at the previous codegraph name is moved on first use. configuration

doctor prints size per kind. Paths under the root: extractors/<language>-<lock hash>/, scip/<name>-v<N>-<project key>-<key>.scip (and .lock), scip/ra-config-<project key>-<hash>.json, scip/swift-v<N>-<build key>.lock, swift-build/<build key>/, ts/<project key>-v<N>-<fingerprint>.json, dart/<project key>-v<N>-<fingerprint>.json, projects/<project key>. v<N> is the cache version. The project key is the first 12 hex digits of the SHA-256 of the resolved root.

InvocationRemoves
clean ROOTentries of that project and of projects indexed below it, every kind except extractors
clean --staleolder cache versions and layouts (names with no cache version or project key), .tmp files older than CG_INDEXER_TIMEOUT + 10 min, SCIP locks with no cache entry
clean --allthe cache root, extractors kept
clean --all --extractorsthe cache root, including extractor installs
--db DBthat graph plus -wal / -shm / -journal (alone, the cache is left as-is)
--dry-runpaths and sizes only; --json is the same list

Nothing outside the cache root is deleted except an explicit --db. Symlinks are removed, not followed. A lock a running cg holds is skipped. A non-SQLite --db is refused. If the root resolves to /, a drive root, $HOME, or a directory above $HOME, clean exits 2 and deletes nothing.