Recipe 03 — Keyword layer from Claude Code
Goal
Answer “where is CLAUDE_CODE_SYNC_SKILLS documented?” without an embedding call. The
keyword layer is an FTS5 index (BM25 ranking) built beside each docset’s vector layer; a query
in mode="keyword" is a sub-token match over the facts layer, returns the unit’s snippet and
its url#anchor, and costs microseconds.
When not to use it
- The question is a paraphrase. “The variable that pulls my claude.ai skills down” contains
none of the tokens in
CLAUDE_CODE_SYNC_SKILLS; use hybrid (recipe-04). - The docset has no facts layer yet. Keyword still works over the raw chunks, but the hits
are chunks, not anchored units —
layerin the reply says which answered. - You want the whole page. The keyword hit tells you which page; the read is the second call below.
Steps
- Call
hub_query_docsetwith the token andmode="keyword".docsetis the store key,<host-slug>__<mirror-stem-slug>— for this mirrorcodeclaudecom__codeclaudecom, not the host name; a friendly name returnsno such docset. The index is built on first use (docset_indexer keyword-indexunder the hood), so the first call on a docset is slower once. - Read
urlandsnippetoff the top hit — those are the fields a keyword hit carries (score,url,seq,snippet);text,unit_typeandorigincome back from the semantic and hybrid legs instead. The URL carries the anchor. - Open the page with
hub_llms_full_read(key, page=<url>). Thatkeyis the llms-full catalog key fromhub_llms_full_list(code.claude.com__docs), a different namespace from the docset key;pagematches by exact source URL or a case-insensitive title substring.
From a Claude Code session with the global_ai_hub server connected:
hub_query_docset(docset="codeclaudecom__codeclaudecom", question="CLAUDE_CODE_SYNC_SKILLS", mode="keyword", top=3)
The reply is JSON. Verbatim, run against this hub on 2026-08-31 — nothing dropped:
{
"docset": "codeclaudecom__codeclaudecom",
"layer": "facts",
"queried": "codeclaudecom__codeclaudecom__facts",
"mode": "keyword",
"results": [
{
"score": 15.2427,
"url": "https://code.claude.com/docs/en/env-vars#variables",
"seq": 8391,
"snippet": " … Timeout in milliseconds for a mid-session skills resync when `[CLAUDE_CODE_SYNC_SKILLS]` is set (default: 30000). Bounds the download triggered when the … "
},
{
"score": 14.651,
"url": "https://code.claude.com/docs/en/env-vars#variables",
"seq": 8392,
"snippet": " … Timeout in milliseconds for the first query to wait for the initial skill list when `[CLAUDE_CODE_SYNC_SKILLS]` is set (default: 5000). When … "
},
{
"score": 14.3472,
"url": "https://code.claude.com/docs/en/env-vars#variables",
"seq": 8390,
"snippet": " … Before it runs the first query, Claude Code waits up to `[CLAUDE_CODE_SYNC_SKILLS]_WAIT_TIMEOUT_MS` for the list of your skills … "
}
]
}
The square brackets are FTS5’s snippet() match markers, not part of the text; the leading
and trailing … are its elision markers, so a snippet is a window around the match rather
than the whole unit.
Read the ranking honestly: all three hits are on the same page, and the variable you asked
for is third. Units 8391 and 8392 are CLAUDE_CODE_SYNC_SKILLS_INSTALL_TIMEOUT_MS and
…_WAIT_TIMEOUT_MS; unit 8390 is CLAUDE_CODE_SYNC_SKILLS itself. BM25 rewards the token in
a shorter field, so the two timeout units outscore the variable they refer to. That costs
nothing here, because all three carry the same url#anchor — keyword tells you which page,
and the anchor is identical whichever of the three you take.
Then, to open the page behind the top hit:
hub_llms_full_read(key="code.claude.com__docs", page="https://code.claude.com/docs/en/env-vars")
That returns an envelope, not raw markdown. The real fields on this call, with text cut
here (it is 20,000 characters):
{
"key": "code.claude.com__docs",
"url": "https://code.claude.com/docs/llms-full.txt",
"page_title": "Environment variables",
"page_url": "https://code.claude.com/docs/en/env-vars",
"total_chars": 475588,
"truncated": true,
"text": "Reference for environment variables that control Claude Code behavior.\n\nEnvironment variables can control Claude Code behavior such as model selection, authentication, request routing, and feature toggles. … ← truncated here for the page; the tool returned 20,000 characters"
}
The # Title / Source: lines are the grammar the tool matches on inside llms-full.txt;
they are lifted into page_title and page_url rather than left in text, so text starts
at the page body. total_chars is the page’s full length and truncated says whether you got
all of it — continue with offset, or raise limit.
The same two calls from the shell, for a script:
.venv/bin/python scripts/docset_indexer.py keyword codeclaudecom__codeclaudecom "CLAUDE_CODE_SYNC_SKILLS" --layer facts --mode phrase --top 3
--mode is any | all | phrase | raw; phrase is what an exact token wants.
Expected output
Every hit’s URL ends in the heading that documents the variable
(/docs/en/env-vars#variables), and the reply’s layer names which layer answered (facts)
while queried names the collection it actually read (…__facts). The corresponding line in
the export’s llms-facts.txt is typed [parameter] — line 11,378 of that file begins
- [parameter] `CLAUDE_CODE_SYNC_SKILLS`: Set to `1` … — but keyword hits do not carry the
type, so ask for mode="hybrid" or "semantic" when you want unit_type on the hit. What a
keyword hit does carry is exactly score, url, seq, snippet, as above.
The full-read then lands you on that page: page_title comes back as "Environment variables", page_url as the URL you asked for, so the anchor from the hit resolves to a
heading you can see in text.
If layer says raw, the docset has no facts layer; the hit is still correct but is a text
chunk without an anchor. That is the signal to run extract → export on it.
Cost
Measured on the hub: the FTS5 lookup is sub-millisecond after the index exists; the first
call on a docset builds the index (seconds for this family’s 14,031 units). Zero model
tokens, zero embeddings. The page read is bounded by limit, which defaults to 20,000
characters — about 5k tokens — and is capped at 200k by the tool. The env-vars page above is
475,588 characters, so the default read came back "truncated": true; a shorter reference
page returns whole.
Runnable in step 4 (playground).