<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Brennan Hitchcock]]></title><description><![CDATA[Problem solving, musings and ideas.]]></description><link>https://hitchcock.dev/</link><image><url>https://hitchcock.dev/favicon.png</url><title>Brennan Hitchcock</title><link>https://hitchcock.dev/</link></image><generator>Ghost 5.75</generator><lastBuildDate>Fri, 29 May 2026 15:09:26 GMT</lastBuildDate><atom:link href="https://hitchcock.dev/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[If your RAG candidate says "cosine similarity" first, end the interview]]></title><description><![CDATA[<p>If your RAG candidate says &quot;cosine similarity&quot; before asking what the queries look like, end the interview.</p><p>I do not mean that as a punchline. The order is telling you something. A candidate who reaches for the embedding model before they reach for the query log is solving</p>]]></description><link>https://hitchcock.dev/if-your-rag-candidate-says-cosine-similarity-first-end-the-interview/</link><guid isPermaLink="false">6a0b78a2d8f9670596477e94</guid><dc:creator><![CDATA[Brennan Hitchcock]]></dc:creator><pubDate>Mon, 18 May 2026 20:41:19 GMT</pubDate><media:content url="https://hitchcock.dev/content/images/2026/05/Gemini_Generated_Image_fucp9wfucp9wfucp.png" medium="image"/><content:encoded><![CDATA[<img src="https://hitchcock.dev/content/images/2026/05/Gemini_Generated_Image_fucp9wfucp9wfucp.png" alt="If your RAG candidate says &quot;cosine similarity&quot; first, end the interview"><p>If your RAG candidate says &quot;cosine similarity&quot; before asking what the queries look like, end the interview.</p><p>I do not mean that as a punchline. The order is telling you something. A candidate who reaches for the embedding model before they reach for the query log is solving a problem they read about, not the one in front of them.</p><p>Last week I wrote about how retrieval quietly fails. Vector search is a primitive, not a search engine, and the systems that hold up under real traffic hybridize, route, and re-rank around that fact. A few people asked the obvious follow-up. How do you hire someone who already knows this?</p><p>I do not have a clean answer. I have six questions. They aren&apos;t a checklist so much as a set of tripwires, places where the candidate&apos;s instincts either match how the system fails in production or they do not. Most of them have nothing to do with embeddings. The last one is the question every candidate should want you to ask.</p><p>Here is what I would run, in order.</p><h2 id="1-what-does-your-query-log-look-like">1. What does your query log look like?</h2><p>Ask this near the start of the conversation, after they have described a RAG system they have built. The answer is binary. Either they have a specific picture in their head, or they do not.</p><p>A good answer sounds something like this. Most queries are short. About a third have an identifier in them. There is a long tail of weird stuff people type when they are frustrated, and last quarter we started bucketing the tail because most of the support escalations were coming from one shape of query. The candidate is describing a thing they have looked at, recently, with their own eyes.</p><p>A weak answer pivots. They start talking about chunking strategy, or which embedding model they chose, or how they tuned chunk overlap. The query log is offstage somewhere, a thing other people deal with.</p><p>Production retrieval is mostly a literacy problem. You cannot fix what you have not bothered to read. Candidates who skip this step build systems that work fine on the examples in the original paper and then fall over on whatever the support team is actually fielding.</p><p>If they have never opened the log, that is the answer. Move on.</p><h2 id="2-show-me-your-eval-set">2. Show me your eval set.</h2><p>Not a benchmark. The actual artifact they opened on Monday morning to see whether Friday&apos;s change made the product better or worse.</p><p>The strongest answer is a candidate who pulls up a CSV. Two or three hundred real queries, labels for what good retrieval looks like on each, a column flagging the hard cases. They can walk you through the queries they argued over, the time they realized their eval set had a bias baked in because it was assembled by one person on a Tuesday, the change that took them by surprise. There is a history with this file.</p><p>The middle answer is a candidate who has an eval but it is the publisher&apos;s example queries, or whatever shipped with the framework. They have measured something other than their product.</p><p>The weak answer is some variation of &quot;we look at the responses and they seem fine.&quot; Or precision and recall numbers with no context for what was being measured against. Or the confident claim that the LLM judges its own answers, which usually means nobody has audited what the judge is doing.</p><p>This is a proxy question for whether retrieval is being treated as a science or as a config file. The candidates who treat it as a science have already lost arguments to their own data. That is what you want.</p><h2 id="3-what-is-the-worst-question-your-system-answers-confidently">3. What is the worst question your system answers confidently?</h2><p>The word doing the work here is &quot;confidently.&quot; Anyone can list the queries that returned nothing. You want the cases where the system returned an answer with full conviction and the answer was wrong.</p><p>A good candidate has at least one of these in their pocket, told with the slight wince of someone who has not fully forgiven themselves for letting it ship. The user asked about policy and got an answer from a deprecated doc. A finance question came back with numbers from the wrong fiscal year. A name collision sent someone to the wrong customer&apos;s record. The example is specific, the log line is specific, sometimes there is a specific complaint attached.</p><p>A weak candidate gives you a category. We had some hallucinations. The model sometimes gets things wrong. We use temperature zero. General statements about LLM behaviour, because they have not lived in any of the specific failures. If they had, they would still be a little upset about one of them.</p><p>This is also a humility test. Candidates who can talk about a confident wrong answer have made peace with the fact that the system is going to do this no matter how well it is tuned. The ones who cannot, often believe the next prompt tweak will fix it, and they argue against guardrails on the grounds that the model should know better. Those candidates are expensive to manage.</p><h2 id="4-when-would-you-not-reach-for-vector-search">4. When would you not reach for vector search?</h2><p>This one is short and it discriminates fast. A strong candidate has a list. Exact match against identifiers. Anything regulatory where the answer has to come from a specific named document. Structured filtering on date ranges, status flags, low cardinality fields where the user has already given you the answer in the query. They will sometimes pre-empt you and say something like, for half the traffic the better tool is a SQL query and we route there before anything else fires.</p><p>A weak candidate pauses. They might mumble about hybrid search, or note that BM25 is still useful, or wave at reranking. The shape of the pause is the signal: they are searching for the right answer instead of recognising it.</p><p>What the question is testing is not the list itself. It is whether they understand vector search as one tool among several, with specific failure modes that other tools handle better. Candidates who already do have probably wired a regex router in front of their retriever. For everyone else, every problem they encounter is going to look like an embedding tuning problem.</p><h2 id="5-what-does-the-failure-look-like-to-the-user">5. What does the failure look like to the user?</h2><p>This pulls the candidate out of the engineer frame, and it is the question most likely to surprise them.</p><p>The answers I am listening for sit at the seam between system design and product. What does the user see when the retrieved documents do not contain the answer at all. What does the user see when the documents do contain it but the LLM rewords the answer into something they misread. What happens when somebody asks a question the system was never going to be able to handle and gets back a long, plausible, completely hallucinated paragraph. Whose desk does the support ticket land on, and how does the user even know something has gone wrong.</p><p>A strong candidate has thought about this and probably argued with someone about it. They might tell you they pushed for an &quot;I do not know&quot; response path and got overruled by product. Or describe a UI tweak they made to surface which document an answer came from, because users were quoting the bot&apos;s response in meetings and getting embarrassed. They have opinions about what should happen at the moment the system fails, because they have watched it fail.</p><p>A weak candidate stays at the model. The LLM hallucinated. The retrieval missed. They will not have a clear picture of what the user did next, because they have been thinking of the system as a model with some plumbing instead of a product with a person at the other end. That is the candidate you can hire as a research engineer. They are not yet the person you want owning a production retrieval system.</p><h2 id="6-walk-me-through-a-recent-retrieval-bug-symptom-to-fix">6. Walk me through a recent retrieval bug. Symptom to fix.</h2><p>This is the universal senior engineer question. It also discriminates most sharply for retrieval work, because so much of the job is debugging things that did not throw an exception.</p><p>What I want is specificity. The symptom should be concrete, ideally something a user reported or something they spotted in a dashboard. The investigation should have a shape: a hypothesis they formed, an instrumentation step that confirmed or killed it, an obvious culprit they ruled out before they found the less obvious one. They can usually tell you the wrong answer they had in their head before they got to the right one, which is the tell that they are remembering the bug rather than constructing it.</p><p>The fix should be small and explained in plain language. The regression test should exist. If they mention that the bug came back six weeks later in a slightly different form, even better. That is how you know it was real, and not a war story that got polished for interviews.</p><p>Vague answers here are disqualifying in a way the other questions are not. Everyone has at least one retrieval bug if they have shipped one of these systems. If a candidate cannot produce one, the most generous reading is that they have not shipped. Less generously, they shipped without noticing what was wrong.</p><h2 id="the-question-every-candidate-should-want-you-to-ask">The question every candidate should want you to ask</h2><p>If you are reading this from the other side of the table, the question to hope for is the last one.</p><p>The question is not easy, but it lets you do the thing you have actually spent the most time on. A retrieval bug story is the closest thing this work has to a portfolio piece. It contains evidence of the production access you have had, the metrics you watched, the colleagues you argued with, the decision you made under time pressure. It is what an interview is trying to extract anyway, told in a form that does not feel like an interrogation.</p><p>If you have one, prepare it the way you would prepare a code sample. Start at the user complaint, walk through what you checked first and why you were wrong, show the moment the real cause clicked, end with the smallest fix that solved it and the regression test you wrote to keep it from coming back.</p><p>If you do not have one yet, that is also useful information about where you are. Build something. Ship it to ten people. Watch a retrieval bug happen on a query you did not anticipate. The interview will get easier, and so will the work.</p><hr><p>The retrieval part of RAG is becoming its own discipline, and it does not look like the role most teams are hiring for. The best people I have worked with on these systems are search engineers with some ML literacy, not the other way around. They know classical IR, they instrument production by reflex, and they are comfortable being wrong in measurable ways, which is the rarest qualification of any of them.</p><p>Hire for that and the rest is teachable. Hire for embedding intuition first and you will end up with a beautifully tuned system that quietly fails on the queries your users actually care about.</p>]]></content:encoded></item><item><title><![CDATA[Building RAG retrieval that doesn't quietly fail on keywords]]></title><description><![CDATA[<p><em>The views and opinions expressed here are my own and do not reflect those of my employer.</em></p><p>A few months ago I built a quick prototype. Semantic search over a stack of internal documentation and ticket history. The usual recipe: chunk, embed, throw it in pgvector, expose a small API,</p>]]></description><link>https://hitchcock.dev/building-rag-retrieval-that-doesnt-quietly-fail-on-keywords/</link><guid isPermaLink="false">6a01e8a96cab9e0441cf3d18</guid><dc:creator><![CDATA[Brennan Hitchcock]]></dc:creator><pubDate>Mon, 11 May 2026 14:49:56 GMT</pubDate><media:content url="https://hitchcock.dev/content/images/2026/05/Gemini_Generated_Image_5cts4u5cts4u5cts.png" medium="image"/><content:encoded><![CDATA[<img src="https://hitchcock.dev/content/images/2026/05/Gemini_Generated_Image_5cts4u5cts4u5cts.png" alt="Building RAG retrieval that doesn&apos;t quietly fail on keywords"><p><em>The views and opinions expressed here are my own and do not reflect those of my employer.</em></p><p>A few months ago I built a quick prototype. Semantic search over a stack of internal documentation and ticket history. The usual recipe: chunk, embed, throw it in pgvector, expose a small API, post a link in a team channel.</p><p>Within two days, a colleague tried to find a specific ticket by its identifier (something like <code>JOB-1245-RB</code>) and got back a confidently ranked list of completely unrelated results. The actual ticket was at rank 47, well past anywhere a real retrieval pipeline would ever look.</p><p>That was the moment I stopped treating vector search as a search engine.</p><h2 id="what-dense-embeddings-actually-do">What dense embeddings actually do</h2><p>When the model embeds <code>JOB-1245-RB</code>, it tokenizes the string into subwords, looks up each token&apos;s dense representation, and reduces them into a single 1500-ish-dimensional vector. That vector lives in a semantic space carved out by training on natural-language pairs. The model has seen &quot;job,&quot; some digits, and the letter pair &quot;RB&quot; in millions of contexts that have nothing to do with our ticket system.</p><p>The result is a vector that points roughly toward &quot;things involving work and identifiers.&quot; The exact string, the only thing my colleague actually cared about, gets averaged across the embedding. Any nearest-neighbor search over that space returns semantic neighbors, not exact-string matches.</p><p>BM25 doesn&apos;t have this problem. An inverted index sees <code>JOB-1245-RB</code> as a literal token, finds the four documents that contain it, ranks them by TF-IDF, and returns them in single-digit milliseconds. The algorithm is from the early 90s and it does not care about embeddings.</p><p>This is the gap where pure-RAG architectures quietly fail. Semantic search is genuinely good at &quot;explain how our auth flow works&quot; or &quot;what did we decide about retries last quarter.&quot; It is bad at &quot;find the document with this string in it.&quot; Your users do both kinds of search and they do not tell you which is which.</p><h2 id="hybrid-retrieval-the-boring-answer">Hybrid retrieval, the boring answer</h2><p>The standard fix is hybrid retrieval. Run BM25 and vector search in parallel, fuse the ranked lists. Reciprocal Rank Fusion (RRF) is the merging algorithm of choice because it doesn&apos;t require normalizing scores between two completely different scoring systems.</p><pre><code class="language-csharp">public static IEnumerable&lt;string&gt; ReciprocalRankFusion(
    IEnumerable&lt;List&lt;string&gt;&gt; rankedLists, int k = 60)
{
    var scores = new Dictionary&lt;string, double&gt;();
    foreach (var list in rankedLists)
    {
        for (int i = 0; i &lt; list.Count; i++)
        {
            scores[list[i]] = scores.GetValueOrDefault(list[i])
                            + 1.0 / (k + i + 1);
        }
    }
    return scores
        .OrderByDescending(kv =&gt; kv.Value)
        .Select(kv =&gt; kv.Key);
}
</code></pre><p>That&apos;s the whole algorithm. Documents that show up in both lanes at decent ranks rocket to the top. Documents that only appear in one lane still get represented if they ranked well.</p><p><em>Failure mode I learned the hard way.</em> RRF is only as good as both of its lanes. If your BM25 index has no stemming, no stopword filtering, and no synonym map, the sparse lane returns noise and drags the merged ranking down. A day spent properly configuring Postgres <code>tsvector</code> paid off more than a week of vector tuning did. Hybrid is not a free upgrade. It is two systems that both need to actually work.</p><h2 id="rerank-what-you-retrieved">Rerank what you retrieved</h2><p>Hybrid gets the right document into the top 50. A cross-encoder gets it into the top 3.</p><p>A bi-encoder (your regular embedding model) encodes the query and the document separately and compares vectors. A cross-encoder takes them together as input and produces a single relevance score. It can attend across both texts simultaneously, which is more expressive. It cannot be precomputed, which is more expensive.</p><p>The typical pattern: pull top-50 from hybrid, run a small cross-encoder over those 50 query-document pairs, re-rank to top-5 or top-10. A MiniLM-class model runs locally in tens of milliseconds. Hosted rerankers from Cohere or Voyage add a network hop and a bill but spare you the GPU.</p><p><em>Failure mode.</em> The throughput cliff is real. At top-50 latency stays inside a normal interactive budget. At top-200 the cross-encoder becomes the bottleneck. Don&apos;t reach for a reranker before squeezing the hybrid retrieval below it. Rerankers amplify good candidates. They cannot fix bad ones.</p><h2 id="query-expansion-for-the-queries-users-write-badly">Query expansion for the queries users write badly</h2><p>The third approach is to rewrite the query before retrieving. Send the user&apos;s query to a small fast model, ask for three or four alternate phrasings, retrieve for all of them, dedupe and re-rank.</p><p>This is cheap (one LLM call, a few cents per thousand queries) and effective on ambiguous queries. Users write search queries badly. They under-specify, over-specify, use the wrong jargon, and ask questions when keywords would work better.</p><p><em>Failure mode.</em> Lazy prompting drifts the intent. &quot;How do I reset my password&quot; gets expanded into &quot;account security best practices&quot; by a model that&apos;s trying too hard, and now you&apos;re retrieving the wrong topic. Constrain the prompt to alternate phrasings only, same intent, no broadening. Evaluate the expansion against a small set of known-good queries before turning it on for everyone.</p><h2 id="buy-versus-build">Buy versus build</h2><p>Every major cloud now ships managed hybrid retrieval with reranking baked in. If retrieval is not your differentiator, the case for buying has gotten strong.</p>
<!--kg-card-begin: html-->
<table>
<thead>
<tr>
<th>Service</th>
<th>Hybrid built-in</th>
<th>Reranker</th>
<th>Self-host</th>
<th>Lock-in</th>
</tr>
</thead>
<tbody>
<tr>
<td>Amazon Kendra GenAI Index</td>
<td>Yes</td>
<td>Yes</td>
<td>No</td>
<td>High (AWS)</td>
</tr>
<tr>
<td>Azure AI Search</td>
<td>Yes</td>
<td>Optional</td>
<td>No</td>
<td>High (Azure)</td>
</tr>
<tr>
<td>OpenSearch + Neural Search</td>
<td>Yes</td>
<td>Optional</td>
<td>Yes</td>
<td>Low</td>
</tr>
<tr>
<td>Elastic + ELSER</td>
<td>Three-way</td>
<td>Optional</td>
<td>Yes</td>
<td>Medium</td>
</tr>
</tbody>
</table>
<!--kg-card-end: html-->
<p>Kendra is the most &quot;just works&quot; option, pre-tuned, around a few hundred dollars a month at the entry tier. Azure AI Search gives you the most knobs and is the obvious choice if you are already in that ecosystem. OpenSearch with Neural Search is the cheaper self-hostable AWS option. Elastic plus ELSER does three-way hybrid (BM25 plus dense plus learned sparse) and is the move if you already have an Elastic license.</p><p>For a small team where retrieval is not a moat, buy one of these. The engineering hours you save go into the application layer where users actually feel the difference.</p><h2 id="what-i-would-build-tomorrow-morning">What I would build tomorrow morning</h2><p>For a team shipping RAG this quarter, with Postgres already in the stack and no managed-service budget:</p><ol><li>Use Postgres <code>tsvector</code> for sparse retrieval and pgvector for dense. One database, two indexes.</li><li>Implement RRF in application code. The whole function is fifteen lines.</li><li>Add a regex router that catches identifier-shaped queries (error codes, ticket numbers, SKUs, version strings) and routes them straight to a direct lookup instead of either retrieval lane. This is the single highest-leverage hour of work in the entire pipeline.</li><li>Skip the cross-encoder until you&apos;ve measured precision and have a reason to add latency. Do not pre-optimize.</li><li>Add query expansion last, when you have enough query log data to know which queries are actually failing.</li></ol><p>If you want a head start, drop the prompt below into Claude, Cursor, or your LLM of choice. It scaffolds the whole pipeline as a single C# service with raw SQL against Postgres. You&apos;ll need to adjust the schema and regex patterns to match your domain, but it gets you most of the way there.</p><pre><code>You are building a RAG retrieval service in C# / .NET 10 with Postgres as the only datastore.

Schema:
- A `documents` table with columns: id (uuid), content (text), 
  embedding (vector(1536)), tsv (tsvector), updated_at (timestamptz).
- `tsv` has a GIN index. `embedding` has an HNSW index via pgvector.

Build a `HybridRetrievalService` class with one public method:

  Task&lt;IReadOnlyList&lt;RetrievalResult&gt;&gt; RetrieveAsync(string query, int topK = 10)

Behavior:

1. Run the query through a regex router that detects identifier-shaped patterns
   (ticket IDs like JOB-\d+-[A-Z]+, error codes like 0x[0-9A-F]+, version 
   strings like v\d+\.\d+\.\d+, etc). Patterns must be configurable via an 
   injected IRegexRouterConfig. If any pattern matches, perform a direct 
   ILIKE lookup against `content`, return up to topK results ordered by 
   updated_at DESC. Skip steps 2-3.

2. Otherwise, run two queries in parallel using Task.WhenAll:
   a. Sparse (BM25-style) via Postgres FTS:
      SELECT id FROM documents
      WHERE tsv @@ plainto_tsquery(&apos;english&apos;, @query)
      ORDER BY ts_rank(tsv, plainto_tsquery(&apos;english&apos;, @query)) DESC
      LIMIT 50;
   b. Dense via pgvector. Use an injected IEmbeddingClient.EmbedAsync(string) 
      to get the query vector, then:
      SELECT id FROM documents
      ORDER BY embedding &lt;=&gt; @query_embedding
      LIMIT 50;

3. Fuse the two ranked lists with Reciprocal Rank Fusion (k=60), 
   return top K with id, content, and the fused RRF score.

Constraints:
- Use Npgsql or Dapper. Raw SQL is fine; skip EF Core for query execution.
- Parameterize every query.
- All public methods accept a CancellationToken.
- Include xUnit tests covering: regex router triggers correctly on known 
  patterns, RRF math is correct on synthetic ranked lists, and the hybrid 
  path runs when no pattern matches.
- Do NOT add reranking, query expansion, or fine-tuned embeddings. 
  Save those for a later iteration.
</code></pre><p>That stack handles the queries pure vector search quietly fails on, costs nothing extra to run, and is debuggable when something breaks. The exotic options (custom-fine-tuned embeddings, LLM-as-retriever, inverted HyDE) are tools to reach for when this baseline is measurably falling short, not before.</p><p>Vector search is a useful primitive. It is not a search engine. Treat the retrieval layer like the engineering problem it is, with multiple tools sized to multiple shapes of query, and your RAG system will get embarrassingly better than the one you shipped by importing pgvector and hoping.</p>]]></content:encoded></item><item><title><![CDATA[AI Dev Tools Keep Shipping Like Every Engineer Has One Project]]></title><description><![CDATA[<p>I tried setting up <code>claude-cognitive</code> last week &#x2014; an attention-based working-memory layer for Claude Code that promises persistent context across sessions on large codebases. The setup guide said fifteen minutes. It took three hours.</p><p>The bug count was small. The pattern behind the bugs is what I want to write</p>]]></description><link>https://hitchcock.dev/ai-dev-tools-multi-project-problem/</link><guid isPermaLink="false">695fba0d6cab9e0441cf3cfe</guid><dc:creator><![CDATA[Brennan Hitchcock]]></dc:creator><pubDate>Thu, 08 Jan 2026 14:17:08 GMT</pubDate><media:content url="https://hitchcock.dev/content/images/2026/01/claude_cognifitive_header.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://hitchcock.dev/content/images/2026/01/claude_cognifitive_header.jpg" alt="AI Dev Tools Keep Shipping Like Every Engineer Has One Project"><p>I tried setting up <code>claude-cognitive</code> last week &#x2014; an attention-based working-memory layer for Claude Code that promises persistent context across sessions on large codebases. The setup guide said fifteen minutes. It took three hours.</p><p>The bug count was small. The pattern behind the bugs is what I want to write about, because this is now the dominant failure mode I see across this generation of AI dev infrastructure: defaults written for the author&apos;s one project, on the author&apos;s one machine, with no plan for anyone else.</p><p>Three specific examples, each one a problem the Unix world solved decades ago.</p><h2 id="1-hardcoded-paths">1. Hardcoded paths</h2><p>The router looks for documentation in <code>~/.claude/systems/</code>, <code>~/.claude/modules/</code>, and so on &#x2014; the global Claude directory. But the setup guide has you create that documentation in your project&apos;s <code>.claude/</code>. These are different directories. The script fails silently when it finds nothing.</p><pre><code class="language-python"># Line 452 in the original context-router-v2.py
docs_root = Path(os.environ.get(&quot;CONTEXT_DOCS_ROOT&quot;, str(Path.home() / &quot;.claude&quot;)))
</code></pre><p>Project-local-first, global-fallback is the convention every dev tool from <code>git</code> to <code>direnv</code> to <code>mise</code> has shipped for years. The fix is fifteen lines:</p><pre><code class="language-python">if os.environ.get(&quot;CONTEXT_DOCS_ROOT&quot;):
    docs_root = Path(os.environ[&quot;CONTEXT_DOCS_ROOT&quot;])
elif Path(&quot;.claude&quot;).exists():
    docs_root = Path(&quot;.claude&quot;)
else:
    docs_root = Path.home() / &quot;.claude&quot;
</code></pre><p>That&apos;s not a feature request. That&apos;s the baseline. Any tool that doesn&apos;t do this assumes you only ever work on one codebase, which has never been true for any working engineer.</p><h2 id="2-hardcoded-keywords">2. Hardcoded keywords</h2><p>The router decides which docs are relevant by matching the prompt against a <code>KEYWORDS</code> dictionary that maps trigger words to documentation files. The default dictionary contained over a hundred entries, all tied to the author&apos;s specific project &#x2014; file names like <code>server-one.md</code>, terms like <code>vram</code>, <code>cuda</code>, <code>trajectory</code>, <code>state machine</code>. On my codebase, every one of them was dead weight.</p><pre><code class="language-python"># Original keywords (lines 79&#x2013;197)
KEYWORDS: Dict[str, List[str]] = {
    &quot;systems/server-one.md&quot;: [
        &quot;server-one&quot;, &quot;gpu&quot;, &quot;local model&quot;, &quot;inference&quot;,
        &quot;vram&quot;, &quot;cuda&quot;, &quot;nvidia-smi&quot;...
    ],
    # ...100+ more project-specific keywords
}
</code></pre><p>The fix is to externalize the config to where the project lives:</p><pre><code class="language-python">def load_project_config():
    config_paths = [
        Path(&quot;.claude/keywords.json&quot;),
        Path.home() / &quot;.claude/keywords.json&quot;,
    ]
    for config_path in config_paths:
        if config_path.exists():
            try:
                config = json.loads(config_path.read_text())
                return (
                    config.get(&quot;keywords&quot;, {}),
                    config.get(&quot;co_activation&quot;, {}),
                    config.get(&quot;pinned&quot;, []),
                )
            except (json.JSONDecodeError, IOError):
                continue
    return ({}, {}, [])

KEYWORDS, CO_ACTIVATION, PINNED_FILES = load_project_config()
</code></pre><p>Each project now gets its own <code>.claude/keywords.json</code> with the trigger words, co-activation map, and pinned files that match its own architecture. This is <code>.eslintrc.json</code> and <code>pyproject.toml</code> and <code>.editorconfig</code>. Per-project configuration in the project, with a sane fallback. It&apos;s the contract that lets a tool work for ten thousand different codebases instead of one.</p><h2 id="3-silent-failures">3. Silent failures</h2><p>When no files reached HOT or WARM status &#x2014; which, given the previous two bugs, was every time &#x2014; the router printed nothing.</p><pre><code class="language-python"># Line 492 in original
if stats[&quot;hot&quot;] &gt; 0 or stats[&quot;warm&quot;] &gt; 0:
    print(output)
</code></pre><p>This makes sense in steady-state production. You don&apos;t want injection noise on every prompt. But during setup, you have no signal that the hook is even running. The log file existed at <code>~/.claude/context_injection.log</code> and contained the answer. I found it by <code>find</code>-ing for anything <code>claude-cognitive</code> had touched in the last hour. That&apos;s not a debugging strategy; that&apos;s archaeology.</p><p>If a tool is going to inject anything into my prompt &#x2014; or fail to inject anything &#x2014; it has to tell me. A single line on stderr (<code>activated 3 docs from .claude/</code> or <code>no docs matched, check .claude/keywords.json</code>) would have saved me the three hours. The fix is one log line, or a <code>--verbose</code> flag, or simply printing the stats banner to stderr the first ten invocations and never again. Any of these costs less to implement than this paragraph cost to write.</p><h2 id="the-pattern">The pattern</h2><p>None of these are <code>claude-cognitive</code> bugs in isolation. They&apos;re the same three defaults &#x2014; global-only paths, hardcoded project assumptions, silent execution &#x2014; showing up across this generation of AI dev tooling.</p><p>There&apos;s a reason. Most of these tools start as one person&apos;s local script that worked well enough for them, then get open-sourced under the assumption that &quot;open source it&quot; and &quot;make it usable by others&quot; are the same step. They are not. The work of going from &quot;works on the author&apos;s machine&quot; to &quot;works on a stranger&apos;s machine&quot; is exactly the unglamorous engineering that gets skipped when a tool is gaining momentum on dev Twitter.</p><p>For the people building these tools: the bar is <code>git</code>. It&apos;s <code>direnv</code>. It&apos;s whatever shell-completion framework you ship. We figured this out a long time ago. Project-local config, sane env-var overrides, loud failures by default. The cost of <em>not</em> having these is measured in hours per user per setup, and the user count is going up fast.</p><p>For the rest of us evaluating: ask the questions before you install. Does it respect a project-local config directory? Does it tell me what it did? Does it have an env var I can use to override defaults without forking? If the answer to any of those is no, you&apos;re going to pay the same three hours I did.</p><p><code>claude-cognitive</code> itself is worth the setup once you&apos;ve patched the defaults. The attention routing &#x2014; files heating up as you mention them, cooling off as the conversation drifts, co-activating related modules &#x2014; is genuinely useful on a codebase large enough that you can&apos;t fit the whole repo in context. For anything smaller, you don&apos;t need it.</p><p>I&apos;ve opened a PR against the <a href="https://github.com/GMaN1911/claude-cognitive?ref=hitchcock.dev">upstream repo</a> to add project-local keyword loading. Whether or not it lands, the broader claim stands: AI-native developer infrastructure is still in its hand-rolled-bash-script phase. The teams that win the next wave aren&apos;t the ones with the cleverest models &#x2014; they&apos;re the ones that figure out the boring part: defaults that don&apos;t assume the user is the author.</p>]]></content:encoded></item></channel></rss>