FOSS · MIT—Open-source agent-swarm, the operating system for all your AI agents→
Back to Blog
October 1, 2026

Continue vs Cody: Which Repo Index Actually Grounds Your Vibe-Coding Agent?

Your agent is only as smart as the 5 chunks it was handed, so stop picking tools by vibes and start auditing the index.

Two Buttons meme: a sweating developer choosing between switching from Continue to Cody and running a 5-minute audit of what the repo index actually contains

You asked your agent to "add a retry to the Stripe webhook handler." It wrote a beautiful new handler in a file that already existed under a different name, imported a helper you deleted last month, and skipped the idempotency check your real handler does on line 40. Classic. The model wasn't dumb. It was blind. It answered from whatever chunks your repo index handed it, and those chunks were wrong.

That failure mode is now the number one complaint about AI coding tools. In the Stack Overflow 2025 Developer Survey, 84% of respondents said they use or plan to use AI tools, yet 46% actively distrust their accuracy (only 33% trust it), and the single biggest frustration, cited by 66%, is "AI solutions that are almost right, but not quite." Almost right is exactly what a badly grounded agent produces.

So this post pits two tools that took very different bets on grounding: Continue, the open-source extension that indexes your repo locally, and Sourcegraph's Cody, which leans on a server-side code search engine. Spoiler: the winner for most indie hackers is not a product. It is knowing what your index actually contains. We'll ship three scripts that let you check.

What does "grounding" actually mean for a coding agent?

Every context engine runs the same four-step pipeline, whatever the marketing says:

  • Chunk: split files into pieces (by lines, or by syntax nodes like functions and classes).
  • Retrieve: pull a candidate pool for your question, using vectors (embeddings), keywords (BM25-style), or both.
  • Rank: cut the pool down to what fits the budget, often with a reranker model.
  • Pack: paste the winners into the prompt next to your question.

Each step can silently drop the one file that mattered. And packing more is not a free fix. The "Lost in the Middle" paper (Liu et al., 2023) found that model performance is often highest when relevant information sits at the start or end of the input, and degrades significantly when it sits in the middle of a long context. Ten mediocre chunks can bury the one good chunk. Precision beats volume, which is why the index matters more than the context window size on the pricing page. We go deeper on budgeting in our context window management deep dive.

How does Continue actually index your repo?

Locally. Legacy @Codebase embedded chunks with transformers.js into ~/.continue/index; agent mode now reads and searches files directly.

Continue's classic retrieval path was the @Codebase context provider. Per Continue's own reference docs, embeddings are computed locally using transformers.js and stored in ~/.continue/index, with index metadata in a SQLite file there. The older docs name the default embedder as all-MiniLM-L6-v2, a small 384-dimension general-purpose model, and note that built-in embedder only ran in VS Code. Retrieval pulls nRetrieve candidates (default 25), optionally reranks them (useReranking, default true), and keeps nFinal (default 5) for the prompt. Indexing respects .gitignore and an extra .continueignore with the same syntax, plus a global one at ~/.continue/.continueignore.

Here is the twist most comparison posts miss: Continue's docs now state that "the @Codebase context provider has been deprecated in favor of a more integrated approach to codebase awareness." The recommended path is agent mode, where the model calls file exploration and search tools itself, backed by rules files in .continue/rules and MCP servers. In other words, Continue moved from "guess the right 5 chunks up front" to "let the agent grep until it finds them."

Why would anyone drop embeddings? Because a small general-purpose embedder is great at "where do we handle payments?" and mediocre at refreshWebhookSecret. Identifiers are rare tokens with exact meaning, and vector similarity happily returns a chunk that is semantically near but literally the wrong function. An agent that greps for the identifier gets it right on the first try.

How does Cody decide what context to send?

Cody Enterprise mixes local IDE context with remote Sourcegraph search, ranked BM25-style. It dropped embeddings in February 2024.

Sourcegraph made the same bet earlier and louder. In "How Cody understands your codebase" (February 15, 2024) they wrote that embeddings had been the backbone of Cody's retrieval stack and that they were "leaving them behind (for now)." Chat context became a combination of local context plus remote Sourcegraph search across the whole codebase, with BM25-adapted ranking. Autocomplete only uses local IDE context. Admins control what Cody may read through cody.contextFilters in site config, with include and exclude rules keyed on RE2 repoNamePattern regexes (Sourcegraph 5.4.0 or later).

That design is genuinely strong for big multi-repo orgs: one central, always-fresh trigram search index beats every laptop maintaining its own vector store. If you want the detailed walkthrough of Cody's indexing behaviour, read our Cody indexing deep dive.

Now the part that matters for indie hackers. Sourcegraph announced on June 25, 2025 that new signups for Cody Free and Pro were closed, and that those plans (plus Enterprise Starter) lost access to Cody on July 23, 2025. Cody Enterprise continues. So if you are a solo builder reading a 2024 "Cody vs X" listicle, the Cody column no longer describes something you can sign up for.

Side by side: the comparison table

DimensionContinueCody (Enterprise)
Where the index livesYour machine, ~/.continue/indexYour Sourcegraph instance (server-side search)
Chat retrievalLegacy: local embeddings + rerank. Current: agent tools that search and read filesLocal IDE context + remote search, BM25-adapted ranking
Autocomplete contextLocalLocal IDE context only
Exclusion config.gitignore + .continueignore"cody.contextFilters": { "exclude": [{ "repoNamePattern": "..." }] }
Tuning knobsnRetrieve: 25, nFinal: 5, roles: [embed], roles: [rerank]Query scoping (repo:, file:), admin site config
Solo-dev availability (Oct 2026)Open source, bring your own modelsFree and Pro ended July 23, 2025; Enterprise only
Typical failureStale or noisy local index, vague queriesVocabulary gaps, repo excluded by filters, branch not indexed

Notice that both tools converged on the same lesson: lexical search over code is a better default than vectors alone. The real question is no longer "which index is smarter" but "is the right file even in the pool, and can my agent find it by name?" Let's measure that instead of guessing.

Script 1: audit what a local index will actually ingest

Scenario: a Next.js SaaS monorepo with a committed pnpm-lock.yaml, a checked-in generated/ Prisma client, and some minified vendor JS. Every one of those files is valid text, so a local indexer happily chunks and embeds it. The result is retrieval that returns 30 lockfile chunks for "how do we pin the Stripe SDK version?" This audit walks the repo with the same ignore semantics Continue documents (.gitignore plus .continueignore), skips binaries with git's own NUL-byte heuristic, and fails CI when generated noise is a big share of the bytes.

// index-audit.ts
// What would a local repo index (Continue's legacy @Codebase, or any
// embeddings indexer) actually ingest from this repo?
// Run: npm i ignore && npx tsx index-audit.ts ./my-saas
import { promises as fs, type Dirent } from "node:fs";
import path from "node:path";
import ignore from "ignore";

type Kept = { rel: string; bytes: number };

// Text files that drown real code in retrieval results.
const NOISY_SUFFIXES = [".min.js", ".min.css", ".map", ".snap", "-lock.json", "lock.yaml", ".lock"];
const NOISY_DIRS = ["dist/", "build/", ".next/", "coverage/", "generated/"];
const NOISE_BUDGET = 0.2; // our threshold, not a Continue constant: tune per repo

async function readOptional(file: string): Promise<string> {
  try {
    return await fs.readFile(file, "utf8");
  } catch (err) {
    if ((err as NodeJS.ErrnoException).code === "ENOENT") return "";
    throw err; // EACCES on an ignore file should be loud, not silently skipped
  }
}

async function looksBinary(abs: string): Promise<boolean> {
  const fh = await fs.open(abs, "r");
  try {
    const buf = Buffer.alloc(8000);
    const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
    return buf.subarray(0, bytesRead).includes(0); // NUL in first 8000 bytes, like git
  } finally {
    await fh.close();
  }
}

function isNoisy(rel: string): boolean {
  return (
    NOISY_SUFFIXES.some((s) => rel.endsWith(s)) ||
    NOISY_DIRS.some((d) => rel.startsWith(d) || rel.includes("/" + d))
  );
}

async function main(): Promise<void> {
  const root = path.resolve(process.argv[2] ?? ".");
  const rootStat = await fs.stat(root).catch(() => null);
  if (!rootStat?.isDirectory()) {
    console.error("Not a directory: " + root);
    process.exit(2);
  }

  const ig = ignore().add(".git");
  ig.add(await readOptional(path.join(root, ".gitignore")));
  ig.add(await readOptional(path.join(root, ".continueignore")));

  const kept: Kept[] = [];
  let binary = 0;
  let unreadable = 0;

  async function walk(dir: string): Promise<void> {
    let entries: Dirent[];
    try {
      entries = await fs.readdir(dir, { withFileTypes: true });
    } catch (err) {
      unreadable++;
      console.warn("skip " + dir + ": " + (err as Error).message);
      return;
    }
    for (const entry of entries) {
      const abs = path.join(dir, entry.name);
      // ignore() only accepts POSIX-style relative paths: absolute or "./x" paths throw.
      const rel = path.relative(root, abs).split(path.sep).join("/");
      if (entry.isSymbolicLink()) continue; // symlink loops are a classic indexer hang
      if (entry.isDirectory()) {
        if (!ig.ignores(rel + "/")) await walk(abs);
        continue;
      }
      if (!entry.isFile() || ig.ignores(rel)) continue;
      const { size } = await fs.stat(abs);
      if (size === 0) continue;
      if (await looksBinary(abs)) {
        binary++;
        continue;
      }
      kept.push({ rel, bytes: size });
    }
  }

  await walk(root);
  if (kept.length === 0) {
    console.error("Nothing indexable. Is .continueignore excluding everything (e.g. a bare '*')?");
    process.exit(1);
  }

  const total = kept.reduce((n, f) => n + f.bytes, 0);
  const noisyBytes = kept.filter((f) => isNoisy(f.rel)).reduce((n, f) => n + f.bytes, 0);
  const share = noisyBytes / total;

  console.log("indexable files: " + kept.length + " (" + (total / 1e6).toFixed(1) + " MB)");
  console.log("binary skipped: " + binary + ", unreadable dirs: " + unreadable);
  console.log("noise share by bytes: " + (share * 100).toFixed(1) + "%");
  for (const f of [...kept].sort((a, b) => b.bytes - a.bytes).slice(0, 10)) {
    const hint = isNoisy(f.rel) ? "  <- candidate for .continueignore" : "";
    console.log("  " + (f.bytes / 1e3).toFixed(0).padStart(7) + " KB  " + f.rel + hint);
  }
  if (share > NOISE_BUDGET) process.exit(1); // make CI fail before the index rots
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

What breaks, and why it is built this way: the ignore package throws on absolute or ./-prefixed paths, which is why every path is normalized to POSIX-relative first (Windows backslashes included). Directories are tested with a trailing slash because a gitignore rule like build/ only matches directories. Symlinks are skipped outright because a node_modules workspace link pointing back at the repo root turns a naive walker into an infinite loop. One known gap: this script only reads the root .gitignore. Monorepos with nested ignore files will show extra files here, so treat the output as an upper bound.

Script 2: see what search-based retrieval finds (and misses)

Cody's move, and Continue's agent mode, both rely on lexical search. To feel how that behaves on your repo, here is a small, honest BM25 probe. It is not Sourcegraph's ranker. It is textbook BM25 (k1 = 1.2, b = 0.75) over 40-line chunks with a code-aware tokenizer, so you can see which questions lexical search nails and which ones fall into a vocabulary gap.

// bm25-probe.ts
// Run inside a git repo: npx tsx bm25-probe.ts "refresh stripe webhook secret"
import { execFileSync } from "node:child_process";
import { readFileSync, statSync } from "node:fs";

const CHUNK_LINES = 40;
const MAX_BYTES = 512_000; // lockfiles and bundles would skew document frequencies
const K1 = 1.2;
const B = 0.75;
const STOP = new Set(["the", "a", "an", "of", "to", "in", "where", "do", "we", "how", "is", "and", "for"]);

type Chunk = { file: string; startLine: number; tf: Map<string, number>; len: number };

function tokenize(text: string): string[] {
  const out: string[] = [];
  for (const raw of text.split(/[^A-Za-z0-9_]+/)) {
    if (!raw) continue;
    // stripeWebhookSecret -> stripe, webhook, secret (and snake_case too)
    const parts = raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[ _]+/);
    for (const p of parts) {
      const t = p.toLowerCase();
      if (t.length > 1 && !STOP.has(t)) out.push(t);
    }
    // keep the compound so an exact identifier still outranks loose word matches
    if (parts.length > 1) out.push(raw.toLowerCase());
  }
  return out;
}

function listFiles(): string[] {
  try {
    // git ls-files already applies .gitignore, including nested ones
    const out = execFileSync("git", ["ls-files", "-z"], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
    return out.split("\0").filter(Boolean);
  } catch (err) {
    console.error("git ls-files failed; run inside a git work tree. " + (err as Error).message);
    process.exit(2);
  }
}

function buildChunks(files: string[]): Chunk[] {
  const chunks: Chunk[] = [];
  for (const file of files) {
    let text: string;
    try {
      if (statSync(file).size > MAX_BYTES) continue;
      text = readFileSync(file, "utf8");
    } catch {
      continue; // deleted-but-still-tracked files and submodule dirs land here
    }
    if (text.includes("\u0000")) continue; // binary
    const lines = text.split(/\r?\n/);
    for (let i = 0; i < lines.length; i += CHUNK_LINES) {
      const tokens = tokenize(lines.slice(i, i + CHUNK_LINES).join("\n"));
      if (tokens.length === 0) continue;
      const tf = new Map<string, number>();
      for (const t of tokens) tf.set(t, (tf.get(t) ?? 0) + 1);
      chunks.push({ file, startLine: i + 1, tf, len: tokens.length });
    }
  }
  return chunks;
}

function search(chunks: Chunk[], query: string, k = 5) {
  const q = [...new Set(tokenize(query))];
  if (q.length === 0) throw new Error("Query is all stopwords. Use an identifier or a domain noun.");
  const N = chunks.length;
  const avgLen = chunks.reduce((s, c) => s + c.len, 0) / N;
  const df = new Map<string, number>();
  for (const c of chunks) for (const t of q) if (c.tf.has(t)) df.set(t, (df.get(t) ?? 0) + 1);

  const missing = q.filter((t) => !df.has(t));
  if (missing.length) console.warn("zero-hit terms (vocabulary gap): " + missing.join(", "));

  return chunks
    .map((c) => {
      let score = 0;
      for (const t of q) {
        const f = c.tf.get(t);
        if (!f) continue;
        const n = df.get(t)!;
        const idf = Math.log(1 + (N - n + 0.5) / (n + 0.5));
        score += idf * ((f * (K1 + 1)) / (f + K1 * (1 - B + (B * c.len) / avgLen)));
      }
      return { where: c.file + ":" + c.startLine, score };
    })
    .filter((r) => r.score > 0)
    .sort((a, b) => b.score - a.score)
    .slice(0, k);
}

const query = process.argv.slice(2).join(" ");
if (!query) {
  console.error('usage: npx tsx bm25-probe.ts "your question"');
  process.exit(2);
}
const chunks = buildChunks(listFiles());
if (chunks.length === 0) {
  console.error("No text chunks indexed. Empty repo, or everything over MAX_BYTES?");
  process.exit(1);
}
try {
  const hits = search(chunks, query);
  if (hits.length === 0) console.log("No lexical hits: an embeddings index might bridge this, grep will not.");
  for (const h of hits) console.log(h.score.toFixed(2).padStart(6) + "  " + h.where);
} catch (err) {
  console.error((err as Error).message);
  process.exit(1);
}

Run it twice. First with an identifier you know exists, like "stripeWebhookSecret rotate": in our experience the right chunk lands at or near the top, because rare identifier tokens get a high IDF and BM25 rewards them hard. Then ask the way you talk: "where do we handle failed payments". If your code says invoice.payment_failed and dunning, the probe warns that failed and payments are weak or zero-hit terms. That warning is the whole lesson. Lexical retrieval is precise when your words match the code's words and useless when they do not. Embeddings exist to close exactly that gap, which is why Continue still lets you plug in an embed model and a reranker.

Edge cases this script handles on purpose: a query made only of stopwords throws instead of returning random chunks; files above 512 KB are skipped because a single lockfile would dominate document frequencies and drag every IDF down; submodule entries from git ls-files are directories, so the read fails and is skipped rather than crashing the run.

Script 3: probe a Sourcegraph instance the way Cody does

If your day job or client runs Sourcegraph, you can query the same search engine Cody pulls remote context from, through the GraphQL API at /.api/graphql. This tells you whether the candidate pool even contains the file before you blame the model. It uses the same SRC_ENDPOINT and SRC_ACCESS_TOKEN variables as the src CLI.

// sg-context.ts
// Env: SRC_ENDPOINT=https://sourcegraph.example.com SRC_ACCESS_TOKEN=...
// Run: npx tsx sg-context.ts 'repo:^github[.]com/acme/api$ stripe webhook secret'
const endpoint = process.env.SRC_ENDPOINT?.replace(/\/+$/, "");
const token = process.env.SRC_ACCESS_TOKEN;
const TIMEOUT_MS = 15_000;
const MAX_RETRIES = 3;

const QUERY =
  "query CtxProbe($q: String!, $pt: SearchPatternType!) {" +
  "  search(query: $q, version: V3, patternType: $pt) {" +
  "    results { matchCount limitHit alert { title description }" +
  "      results { __typename ... on FileMatch {" +
  "        repository { name } file { path }" +
  "        lineMatches { preview lineNumber } } } } } }";

type LineMatch = { preview: string; lineNumber: number };
type FileMatch = {
  __typename: "FileMatch";
  repository: { name: string };
  file: { path: string };
  lineMatches: LineMatch[];
};
type SearchResults = {
  matchCount: number;
  limitHit: boolean;
  alert: { title: string; description: string } | null;
  results: Array<FileMatch | { __typename: string }>;
};
type Resp = { data?: { search: { results: SearchResults } | null }; errors?: Array<{ message: string }> };

async function gql(variables: Record<string, string>): Promise<Resp> {
  for (let attempt = 0; ; attempt++) {
    const ctrl = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
    try {
      const res = await fetch(endpoint + "/.api/graphql", {
        method: "POST",
        headers: { Authorization: "token " + token, "Content-Type": "application/json" },
        body: JSON.stringify({ query: QUERY, variables }),
        signal: ctrl.signal,
      });
      if (res.status === 401 || res.status === 403) {
        throw new Error("Auth failed (" + res.status + "): token missing, expired, or not allowed here.");
      }
      if ((res.status === 429 || res.status >= 500) && attempt < MAX_RETRIES) {
        const retryAfter = Number(res.headers.get("retry-after"));
        const waitMs = retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt;
        await new Promise((r) => setTimeout(r, waitMs));
        continue;
      }
      if (!res.ok) throw new Error("HTTP " + res.status + ": " + (await res.text()).slice(0, 200));
      return (await res.json()) as Resp;
    } catch (err) {
      // cold searches on big monorepos can exceed the timeout once, then hit cache
      if ((err as Error).name === "AbortError" && attempt < MAX_RETRIES) continue;
      throw err;
    } finally {
      clearTimeout(timer);
    }
  }
}

async function main(): Promise<void> {
  const q = process.argv.slice(2).join(" ");
  if (!endpoint || !token) throw new Error("Set SRC_ENDPOINT and SRC_ACCESS_TOKEN.");
  if (!q) throw new Error("Pass a search query, ideally scoped with repo:.");
  const query = /\bcount:/.test(q) ? q : q + " count:50";

  // "keyword" is the newer pattern type; older instances reject it as an enum value.
  let resp = await gql({ q: query, pt: "keyword" });
  if (resp.errors?.some((e) => /patternType|SearchPatternType|keyword/i.test(e.message))) {
    console.warn("keyword pattern type unsupported here, falling back to standard");
    resp = await gql({ q: query, pt: "standard" });
  }
  if (resp.errors?.length) throw new Error("GraphQL: " + resp.errors.map((e) => e.message).join("; "));

  const r = resp.data?.search?.results;
  if (!r) throw new Error("Empty search payload. Check the query syntax.");
  if (r.alert) console.warn("alert: " + r.alert.title + " - " + r.alert.description);
  if (r.limitHit) console.warn("limitHit: pool truncated. Narrow with repo: or file: before trusting ranks.");

  const files = r.results.filter((x): x is FileMatch => x.__typename === "FileMatch");
  if (files.length === 0) {
    console.log("0 file matches: is the repo indexed on this instance, and is the branch searchable?");
    return;
  }
  for (const f of files.slice(0, 15)) {
    const first = f.lineMatches[0];
    const line = first ? first.lineNumber + 1 : 1; // API line numbers are 0-based
    const preview = first ? first.preview.trim().slice(0, 100) : "";
    console.log(f.repository.name + "/" + f.file.path + ":" + line + "  " + preview);
  }
  console.log("matchCount=" + r.matchCount + ", showing " + Math.min(15, files.length) + " files");
}

main().catch((err) => {
  console.error((err as Error).message);
  process.exit(1);
});

The details that bite in production: LineMatch.lineNumber is zero-based, so without the + 1 every link you paste into your editor is off by one. A limitHit means the pool was truncated before ranking, so "Cody didn't find it" may really mean "it was result 51 of 50." And an instance on an older release may reject keyword as a pattern type, so the script retries with standard instead of dying. Finally, remember that cody.contextFilters governs what Cody may use as context, not what search returns. If this script finds the file but Cody never cites it, the filter config is your first suspect.

The Continue setup we actually recommend

If you stay on Continue, give the agent good search tools, a map, and an optional semantic fallback. The embed and rerank roles below follow Continue's documented config.yaml model roles. Both roles must be added explicitly, because the default role list is chat, edit, apply and summarize.

# .continue/config.yaml (excerpt, illustrative keys)
models:
  - name: Voyage Code 3
    provider: voyage
    model: voyage-code-3
    apiKey: <VOYAGE_API_KEY>
    roles:
      - embed
  - name: Voyage Reranker
    provider: voyage
    model: rerank-2
    apiKey: <VOYAGE_API_KEY>
    roles:
      - rerank

# .continueignore (same syntax as .gitignore)
pnpm-lock.yaml
**/*.min.js
**/*.map
generated/
coverage/
.next/

# .continue/rules/repo-map.md
---
name: Repo map
alwaysApply: true
---
- Stripe webhooks: apps/api/src/billing/webhooks.ts (idempotency via processed_events table)
- Never import from src/legacy/*; it is deleted on the next release
- Search for identifiers with the search tool before creating any new file

The rules file is the cheapest grounding you will ever buy. It turns the vocabulary gap from Script 2 into a lookup table: "failed payments" now maps to a real path, and the agent stops inventing a second webhook handler.

Troubleshooting: why your agent still cites the wrong file

Work through these in order. Each one has a specific check.

  • The file was never indexed. Run Script 1 and grep its output for the path. Common culprits: a broad .continueignore pattern, a file that trips the binary check (a stray NUL byte in a fixture), or an ignore rule inherited from the global ~/.continue/.continueignore you forgot about.
  • The index is stale. Local indexes track your working tree, and in our experience a big branch switch or rebase can leave it describing code that no longer exists. Trigger a re-index from Continue, or as a last resort quit the IDE and delete ~/.continue/index to force a full rebuild.
  • The query has a vocabulary gap. Script 2 prints zero-hit terms. If the question uses words the code never uses, rephrase with the identifier, add a rules-file mapping, or enable an embed model.
  • The pool was truncated. On Sourcegraph, Script 3 prints limitHit. Scope with repo: and file: so ranking sees the real candidates.
  • Ranking buried the winner. With nFinal at 5, the sixth-best chunk never reaches the model. Raising it helps recall, but more chunks also push the right one toward the middle of the prompt, where "Lost in the Middle" showed models attend worst. Raise it in small steps and re-test the same question.
  • Cody finds it in search but not in chat. Check cody.contextFilters. RE2 patterns need the dot escaped or wrapped, so ^github[.]com/acme/.+ matches what you intend while an unescaped dot matches any character.

Edge cases and gotchas

  • JetBrains users: Continue's older docs note the built-in transformers.js embedder was VS Code only, so on JetBrains you need a configured embed model or you get no semantic retrieval at all.
  • Autocomplete is not chat: Cody autocomplete uses only local IDE context. A perfectly indexed remote repo does nothing for your tab completions.
  • Generated code is a trap both ways: ignore it and the agent cannot see your Prisma types; index it and it floods retrieval. Index the schema file, ignore the generated client.
  • Secrets in the index: anything indexed can be sent to a model. Add .env* and fixture dumps to .continueignore even if your .gitignore seems to cover them: one missed variant like .env.production.local is enough for a tree walker to pick it up.
  • Old listicles: any comparison that prices Cody Pro is describing a plan that stopped working on July 23, 2025.

Which one should a solo builder pick in 2026?

Our opinionated take: if your team already pays for Sourcegraph Enterprise, Cody's search-first context is excellent across many repos, and Script 3 is how you keep it honest. Everyone else, which is most indie hackers, should run Continue (or any open agent) in agent mode with three habits: a lean .continueignore that Script 1 keeps lean, a rules file that maps your domain words to real paths, and an optional embed plus rerank model for intent-style questions. The index that grounds your agent is the one you can inspect, measure, and fix in five minutes on a Sunday night.

And whatever you pick, verify the output, not the vibes. Grounded agents still ship "almost right" code, so put real end-to-end tests between the agent and your users.

Ready to ship your next project faster?

Desplega.ai helps indie hackers and solopreneurs build and ship faster, with AI-driven testing that catches the bugs your agent was confidently wrong about.

Get Started

Frequently Asked Questions

Can I still use Cody on a free plan as a solo developer?

No. Sourcegraph ended Cody Free and Pro on July 23, 2025, and pointed users to Amp. Cody Enterprise continues, so Cody only fits if your team already runs Sourcegraph.

Is Continue @Codebase still the right way to give an agent repo context?

Continue docs now mark @Codebase as deprecated and point to agent mode, which explores files with search tools, plus rules files and MCP servers. Treat embeddings as optional.

Are embeddings or keyword search better for code retrieval?

Keyword search wins when you know identifiers, embeddings help when you only know intent. Mixing both with a reranker covers more, but a noisy index hurts either approach.

Why does my agent ignore a file I know is relevant?

Usually the file was never indexed (ignored, binary, or too big), the index is stale after a branch switch, or the query uses words that never appear in the code. Probe each in turn.