Explore unfamiliar code
Reading unfamiliar code through an agent burns tokens fast: whole-file dumps to “get oriented”, re-reads to find where a type lives, and log-sized context for what a 40-line function does. CtxCtl inverts the flow — map first, then read only the territory you need. All commands below are read-only and stateless; nothing is indexed or cached.
Start with the outline
Section titled “Start with the outline”outline prints one line per symbol — kind, name, line range, signature — plus token-savings
stats. It is the cheapest orientation pass over any supported file:
ctxctl outline src/server.rs# src/server.rs [12 symbols, ~2.1 KB -> ~410 tokens, saved ~80%] fn handle_request L:42-58 pub async fn handle_request(&self, id: u64) struct RequestHandler L:12-40 pub struct RequestHandler { const MAX_RETRIES L:60 const MAX_RETRIES: u32 = 3;From this alone the agent already knows the file’s shape without reading it. Very large files
fold their symbol list past a threshold (default 50) into a ... [N symbols omitted] marker;
the header always keeps the total count. --no-doc drops doc comments if you want the bare
minimum.
Narrow with –signature and –compact
Section titled “Narrow with –signature and –compact”Once the outline names the interesting symbol, you rarely need its whole body immediately:
# Declaration line onlyctxctl symbol src/server.rs --name handle_request --signature
# Signature + fold marker for the bodyctxctl symbol src/server.rs --name handle_request --compact--compact returns an AST-pruned view: the signature (plus decorators on python), a
// ... [N lines omitted] fold marker (# … for python), and a bare closing line when present.
It is byte-stable and re-parses cleanly, so downstream tooling can still consume it.
Both flags answer the common agent question — “what does this take and return?” — for a fraction of a full read.
Read the body only when needed
Section titled “Read the body only when needed”For the real implementation, symbol without extra flags returns the verbatim slice of that
symbol from the original source:
ctxctl symbol src/server.rs --name handle_request# handle_request src/server.rs:42-58 (58 tokens, saved ~85%)pub async fn handle_request(&self, id: u64) -> Result<String, Error> { let row = self.db.get(id).await?; Ok(row.to_string())}The slice preserves comments, formatting, and indentation exactly — it is source, not a summary. Useful refinements:
--kind <k>narrows same-named matches (a method versus a local variable). With duplicates, the first in source order wins unless narrowed.--lines 3-10sub-ranges within the symbol once you have located the exact lines.- Exit code 4 means the symbol was not found — usually a typo or the wrong file, not a missing feature.
For ranges outside any single symbol — a block of module docs, a config table — use read,
which needs no AST at all:
ctxctl read src/server.rs --lines 100-150ctxctl read src/server.rs --lines 100-150,200-210 # multiple ranges, comma-separatedOpen-ended ranges clamp to the end of the file (--lines 10-). Exit code 2 means the range was
invalid or out of bounds.
Trace imports with deps
Section titled “Trace imports with deps”Before jumping across files, check what the file depends on. deps lists imports with kind and
line number:
ctxctl deps src/main.rs# src/main.rs [3 imports, ~512 B -> ~64 tokens, saved ~88%]external serde L:1local crate::lib L:2The local / external split tells the agent which jumps stay inside the repository (worth an
outline) and which cross into dependencies (usually not worth reading at all).
Non-code files work too
Section titled “Non-code files work too”The same workflow covers more than source code — the symbol engine has backends for markup and styles:
- Markdown: every ATX/setext heading is a symbol, and a heading’s slice spans its whole
section — so
symbol README.md --name Installreturns exactly that section. - CSS/SCSS: rulesets are symbols (the selector list is the name), nested
@mediablocks included. - HTML: elements carrying an
idattribute are addressable by that id.
Agents exploring docs-heavy repositories get the same outline-then-slice discipline without any special casing.
The loop
Section titled “The loop”A complete cheap-exploration pass looks like this:
ctxctl outline src/server.rs # mapctxctl deps src/server.rs # what it hangs offctxctl symbol src/server.rs --name handle_request --compact # shape of the targetctxctl symbol src/server.rs --name handle_request # body, verbatimctxctl read src/server.rs --lines 100-110 # anything outside symbolsEach step’s output ends with the next step’s decision point, and every step is far smaller than the file. For making command output just as small, see Compress command output; the reasoning behind slices-not-summaries is in Principles.