Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Search and query past Claude Code, Codex, Kimi Code, and Pi session history. Reactive: when the user asks "how did I fix X", "what did we do last time", "find the session where", "上次怎么修的", "之前的session", "历史记录". Proactive: when the user references past work you lack context for, when you're about to modify a file with complex edit history, when the user says "继续之前的" or "continue where we left off", or when understanding prior decisions would improve your current response. Memory: when the user sa
.claude/skills/tommy0103-obelisk/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 721% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 503% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 306% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 599% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 461% | 0% |
Search and query local Claude Code, Codex, DeepSeek Harness, GitHub Copilot, Kimi Code, OMP, Pi, and ZCode session history. Obelisk indexes sessions, messages, tool calls, tool results, summaries, subagents, workflows, workflow agents, parent chains, and raw JSONL lines into SQLite + FTS5.
Obelisk has several transcript sources. Treat all of them as ordinary sessions by default: Claude rows use source='claude', Codex rows use source='codex', DeepSeek Harness rows use source='deepseek', GitHub Copilot rows use source='copilot', Kimi Code rows use source='kimi', OMP rows use source='omp', Pi rows use source='pi', and ZCode rows use source='zcode'. Use source only when provenance matters or the user asks to scope to one provider. Provider-specific records are projected into the same canonical tables; some providers may not emit every kind of subagent or workflow metadata.
Obelisk is a CodeAct memory layer: write a small JS query, run it locally, read the JSON, then answer. Do not turn history into a flat document or browse entire sessions by default.
Treat a successful Obelisk invocation as a precondition for using its results. Obelisk refreshes ~/.obelisk/obelisk.sqlite before retrieval so the answer includes the latest sessions and can identify the invoking session. Never degrade to a stale, read-only index when that refresh cannot write.
In Codex workspace-write or managed sandboxes, ~/.obelisk may be outside the writable roots. If an Obelisk command fails with SQLITE_READONLY, EACCES, EPERM, attempt to write a readonly database, or another permission error for ~/.obelisk, rerun the same command with sandbox_permissions: "require_escalated". On another agent host, use its equivalent mechanism to grant write access to ~/.obelisk. Do not replace the failed command with direct SQLite, JSONL, or stale-index reads.
If write access is unavailable or the user denies escalation, stop and report the permission blocker. Retrieval is complete only when the Obelisk command exits successfully and returns its query result.
Fast keyword search (pass a unique nonce so Obelisk can recognize your own session in results). Invent the nonce yourself and type it as a literal token: the transcript records the command as typed, so a shell substitution like $(uuidgen) never expands there and can never resolve:
bashobelisk --search "keyword" --nonce "obq-<unique-token-you-invent>"
Custom query:
heredoc both work):
bash qdir=$(mktemp -d /tmp/obq.XXXXXX 2>/dev/null || { d="/tmp/obq.$$.$RANDOM"; mkdir "$d"; echo "$d"; }) qfile="$qdir/query.mjs"
The .mjs name lives inside the unique directory, so the mktemp template always ends on the X run (BSD mktemp requires that).
bash obelisk --query "$qfile"
Self-identification matches the file path when the transcript contains it, and falls back to the script content — heredoc/Write tool-call records carry it verbatim, so a path hidden behind $qfile still resolves.
The query file runs inside (async () => { ... })(). Use return to emit JSON. Query scripts are read-only: remember() and forget() are not available, and sql() only accepts read-only SELECT/WITH queries.
Obelisk refreshes the index before each query, so your own live session shows up in results. The invocation nonce (a literal --search --nonce token, or the --query file path with script content as fallback) lets Obelisk mark it: session projections in search() hits and sessions() rows carry is_invoking: true, and overview().current.session_id holds the invoking session id when known. Treat a session flagged is_invoking as your own current context, NOT as independent historical evidence. Resolution is newest-wins over recent matches; only a near-simultaneous same-nonce collision (or no match at all) leaves nothing marked and current.session_id null — identity is honestly unknown.
Start with helpers, not raw SQL. For the first Obelisk query in a task, normally call overview({ limit: 6 }) unless the user already gave an exact session_id, message uuid, or absolute file path.
For semantic or synthesis tasks, combine orientation, memory recall, and raw session evidence before deciding whether a detail pass is needed:
jsconst map = overview({ limit: 6 }); const project = map.current.project?.project; const topic = 'English topic terms translated from the user request'; return { orientation: map.current_project, prior_memories: memories({ project, query: topic, limit: 5 }), session_evidence: search(topic.replace(/[-_]/g, ' '), { project, limit: 8 }), };
Use sql() only as an escalation path for exact joins, aggregations, or schema questions that helpers cannot express cleanly. Do not use raw SQL as a generic fallback for broad retrieval.
Obelisk supports a small intent prefix layer after /obelisk. This is for output intent, not retrieval architecture.
| Intent | Description | Reference | |---|---|---| | recap [target] | Generate weekly/monthly recap card content for app handoff or share-style output. | references/recap/overview.md |
Routing rules:
recap, read references/recap/overview.md before thefirst query. Everything after recap is the recap target. Common app-generated prompts include /obelisk recap this week, /obelisk recap last week, /obelisk recap this month, and /obelisk recap last month; interpret these as natural period targets relative to the current date and timezone.
recap does not create a separate retrieval layer. It still usesoverview(), memories(), helpers, and sql() only when needed.
pattern and writing file; retrieve that card's evidence, read that card's writing file, update the JSON, then move to the next card. Do not preload all recap references before the current card is written.
recap, do not loadreferences/recap/overview.md. Continue with Query Routing below. Do not infer recap from broad requests for weekly/monthly summaries, charts, rankings, shareable cards, or playlist-style metaphors.
Use references by job, not by habit:
| Reference | Use when | |---|---| | references/query-patterns.md | Broad synthesis, progress summaries, design history, weekly/monthly reviews, approved memory write/archive/update scripts, or questions about what the user did/learned/decided/tried/abandoned. | | references/retrieval-semantics.md | Multi-step retrieval, scoped project/file/session searches, or when scope/artifact/semantic boundaries affect query design. | | references/schema.md | Raw SQL field and join quick reference before writing non-trivial sql(). | | references/api-reference.md | Helper signatures, option names, return fields, or exact remember() / forget() parameter details are unclear. | | references/pitfalls.md | Error recovery, FTS syntax, aliases, ordering, row-shape surprises, or compact/raw tradeoffs. | | references/recap/overview.md | Explicit /obelisk recap ... requests only. |
Before writing a query, classify the task. Progressive disclosure is useful, but skipping the relevant reference usually costs extra query rounds.
references/query-patterns.md before the first query for broad synthesis, progress summaries, design history, ordinary weekly/monthly reviews, or questions that ask what the user did, learned, decided, tried, or abandoned. Start from the first-pass or one-shot synthesis pattern, then run a faceted detail pass if needed.references/retrieval-semantics.md before multi-step retrieval, scoped project/file/session searches, or synthesis/conclusion/history questions. It defines the query design frame.references/schema.md before raw sql() unless the needed table/column relationship is already explicit here. It is intentionally short and SQL-focused. Do this before running the SQL, not after a missing-column error. Do not start with raw SQL for broad synthesis unless helpers cannot express the needed aggregation or join.references/api-reference.md when helper option names, return fields, scalar shorthand behavior, or remember()/forget() details are unclear.references/pitfalls.md after an error or when FTS syntax, aliases, ordering, row shapes, or compact/raw tradeoffs are unclear.If a helper row shape is unclear, first run a tiny scoped query and return Object.keys(row) or a compact sample. Do not invent field names.
For approved memory mutations, follow the Memory Layer section below first. Use references/query-patterns.md for copyable --attune scripts (Attune Approved Memory, Forget Approved Memory, Update Approved Memory), and references/api-reference.md only for exact parameter semantics.
search(text, opts?)Full-text search across main messages, subagent messages, and workflow-agent messages.
Returns:
js[{ message: { uuid, text, content_type, is_meta, role, timestamp, model, cwd, visibility, source }, session: { id, title, project, started_at, source, is_invoking? }, rank, context }]
session.is_invoking is true only when the hit belongs to the session that ran this query (see "Your Own Session In Results"); it is omitted otherwise.
context here means temporal neighbors: nearby messages in the same session by timestamp. It is not the parent chain. Use context(uuid) or trace(uuid) for causal/parent-chain context.
Use message.content_type to keep evidence boundaries intact: text is user/assistant visible language, thinking is trace/debug material, tool_use marks a tool-call message whose details live in tool_calls, and tool_result marks a tool-result message whose details live in tool_results. unknown is a conservative fallback. Do not treat thinking as a user-visible assistant conclusion. Real user input is type='user' plus content_type='text'; do not invent a separate user_message content type.
Use message.is_meta to separate transcript control-plane material from conversation evidence. is_meta=1 marks injected caveats, command envelopes, or other messages that entered the transcript as user-role content but should not be treated as the user's request by default. search() and thread() omit meta messages unless includeMeta: true is passed; context() and trace() preserve the current causal chain and expose is_meta on returned rows.
Pi and OMP can preserve a branch that was tried and later superseded as visibility='inactive'. Other sources either do not record supersession in their transcripts or discard it while indexing, so an empty inactive result never means nothing was abandoned -- only that this source cannot say. Default helpers return only visible evidence. Pass includeInactive: true to search(), context(), trace(), thread(), summaries(), raw(), fileHistory(), or failures() only when the abandoned path matters. Every returned message or evidence row is labeled with visibility; describe inactive evidence as something tried and then superseded, never as the final decision. hidden is reserved for display-suppressed or transport-only records and is never returned by these helpers, even with the option enabled.
Opts: { limit, sessionId, project, after, before, cwd, source, includeMeta, includeInactive }.
project is a SQL LIKE filter over sessions.project, not an exact project identity. Results are already ordered by FTS5 rank; lower rank sorts earlier. Prefer returned order over manually interpreting numeric rank unless you are deliberately using FTS5 semantics.
source can be 'claude', 'codex', 'deepseek', 'kimi', 'omp', 'pi', 'zcode', or omitted. Omitted means search all indexed sources.
context(uuid, opts?)Returns the full story around one indexed message:
js{ message, parentChain, session, subagent, workflow }
Use this after search() finds a promising message. It is the usual way to expand vertically from one evidence point without dumping the whole session. The target and returned ancestors must be visible by default. Pass { includeInactive: true } to follow an explicitly superseded Pi or OMP path.
sql(query, ...params)Read-only SQL SELECT/WITH with ? placeholders. Returns array rows. SQL is an escape hatch for exact structured joins and aggregations after the helper-first surface is insufficient; it is not the default retrieval entry point.
Before writing non-trivial SQL, read references/schema.md. It is the raw SQL field/join quick reference. The executable DDL is CLI-owned and is deliberately not duplicated in this docs-only skill. Common safe joins:
tool_calls does not have timestamps. Join messages m ON m.uuid = tc.message_uuid.tool_results does not have timestamps. Join messages m ON m.uuid = tr.message_uuid.sessions s ON s.id = <table>.session_id.GROUP BY, COUNT, MAX, ORDER BY, and LIMIT over hand-counting in the final answer.Tables: sessions, messages, tool_calls, tool_results, summaries, memories, subagents, workflows, workflow_agents, messages_fts.
These helpers are convenience accessors over the same SQLite structure. They do not replace sql(), but they are the default first-pass surface. Use sql() when you need an exact aggregation or a join the helper does not expose.
All list helpers accept a bounded limit. Many also accept: { project, after, before, sessionId, sessions, branch, source }. Check references/api-reference.md or a tiny sample before relying on less common filters or return fields.
overview(opts?) -- compact orientation map. Returns current cwd/project if knowable, the invoking session id (current.session_id) when the invocation nonce resolved, global project/source counts, and current-project recent sessions plus memory records. It is a map, not evidence.sessions(opts?) -- session rows, newest first. project is a SQL LIKE pattern. message_count counts the visible canonical transcript; inactive and hidden records are excluded. The invoking session row carries is_invoking: true.recent(n?) -- shorthand for recent sessions.summaries(opts?) -- summary rows, newest first: { id, session_id, timestamp, source, content, visibility, session_title, project }; inactive rows require includeInactive: true, hidden rows are never returned, and source is the summary kind rather than the transcript provider.subagents(opts?) -- subagent metadata plus messageCount.workflows(opts?) -- workflow runs, newest first.workflowTree(runId) -- workflow row plus parsed result and agents; may include bulky script and result_json, so project compact fields.fileHistory(filePath, opts?) -- Read/Edit/Write tool calls for a file, oldest first; includes many Read rows and labels each result with visibility.failures(opts?) -- failed tool results with tool/session context and visibility, newest first.trace(uuid, opts?) -- parent chain from root to message.thread(sessionId, opts?) -- session messages ordered by timestamp, omitting meta messages by default. Pass { includeMeta: true } for injected context or { includeInactive: true } for superseded Pi or OMP history.raw(uuid, opts?) -- windowed source access for one visible message. Pi returns the selected source-message container whether it was stored directly or inside a retained tail. Inactive targets require includeInactive: true; hidden targets return null.memories(opts?) -- recall memory layer. opts: { query, project, sessionId, sessions, after, before, branch, limit }. Without query, returns active memory records newest first. With query, searches summary/path through safe FTS5 tokenization and returns rank; lower rank sorts earlier. Records may include nullable JSON anchors for explicit recall surfaces such as files. Read the file at path for full content.Keep queries scoped, bounded, and structural.
overview({ limit: 6 }) before deeper retrieval unless the user gave an exact session/message/file locator. It is a navigation map; confirm facts with memories(), search(), helpers, or, only when needed, sql().overview(), memories(), search(), sessions(), summaries(), fileHistory(), and other helpers for first-pass retrieval. Escalate to raw sql() only when helpers cannot express the needed join, grouping, or exact schema-level check.session_id, uuid, tool_call_id, run_id, agent_id) and short snippets, then synthesize in the final answer.is_meta=1 rows are injected/control-plane transcript material. Helpers hide them by default; raw SQL for ordinary conversation evidence should include COALESCE(m.is_meta,0)=0 unless meta rows are the investigation target.memories() does not already cover it, explicitly offer to write a memory. Keep the offer brief. Do not write the markdown file or run --attune until the user approves.If field, context, ordering, FTS, or helper semantics affect the query, read references/retrieval-semantics.md before coding. If a query errors, read references/pitfalls.md before retrying.
Obelisk has a persistent memory layer alongside raw session data. Every retrieval queries both layers: memories() for prior conclusions, search() and helpers for raw session evidence. Use memory as prior notes, not final authority. If a memory record influences your answer, say naturally that it was previously recorded, and compare it with raw session evidence when correctness depends on it. Raw session data is the evidence layer, but one hit is not a complete truth; query and cite it compactly.
The memory layer is English-indexed. Use English terms in memories({ query }) even when the user asks in another language. Write every remember().summary in English, regardless of the current conversation language. The runtime rejects obvious CJK text in memory queries and summaries as a guardrail.
Recall: query memories({ query: 'English topic terms', project: '...' }) to find prior conclusions relevant to the current task. Translate non-English user requests into concise English query terms before calling memories(). Memory recall uses safe FTS5 tokenization over summary and path, so hyphens/punctuation are tokenized instead of causing raw MATCH syntax errors. Like other list helpers, passing a string is treated as sessionId, and passing a number is treated as limit. Read the file at path for full content. memories() returns active memories only. An archived memory is management/audit data, not recall data.
Good memory candidates include design decisions, project conventions, abandoned alternatives, repeated failure causes, workflow patterns, and conclusions synthesized across multiple raw evidence points. Do not propose memory for one-off lookups, uncertain findings, or conclusions already covered by existing memories.
Mutation approvals: judging whether to use a memory in the current answer is an agent decision and does not require approval. Persistent memory changes do. If the user explicitly says a memory is wrong, outdated, should be forgotten, or should now say something else, that request is the approval to archive or update the exact matching memory. Do not ask for a second confirmation unless multiple memories could match. If you notice a possible conflict yourself, explain it briefly and ask before changing memory state.
Writing memories: after a retrieval produces a conclusion worth persisting, propose writing a memory file. The user must approve. Flow:
Write tool (user approves).remember() in a narrow memory-registration script:jsreturn remember({ path: '.obelisk/memories/design-decision-x.md', session_id: 'current-session-id', message_start: 'uuid-of-first-relevant-msg', message_end: 'uuid-of-last-relevant-msg', anchors: [{ kind: 'file', path: 'src/path/to/file.ts' }], summary: 'Detailed summary: what was decided, why, what alternatives were considered, and what constraints drove the choice.' })
Run the registration script with:
bashobelisk --attune /tmp/register-memory.mjs
--attune exposes only memory mutation helpers: remember() and forget(). It does not expose search(), sql(), memories(), or other retrieval helpers. If you need source IDs or memory IDs, find them first with a normal --query script.
remember() validates that path already exists and points to a file. Relative paths are resolved against the source session's project_path when session_id is provided, then stored as normalized absolute paths. Prefer project-relative paths such as .obelisk/memories/... plus session_id. Optional anchors must be an array of objects and is stored as nullable JSON text. Use it only for explicit recall surfaces, such as files associated with the memory.
summary must be English and detailed enough that memories() results alone can judge relevance without reading the file. Include the decision, the reasoning, and the key constraints — not just a title.
The message_start/message_end range marks where in the conversation this conclusion was drawn. Use it later to trace back to the original evidence.
Forgetting memories: if the user says a memory is outdated, wrong, or should be forgotten, use normal recall first to identify the exact memory ID. If there is exactly one clear candidate, the user's request is approval to archive it. If multiple memories could match, ask which one to forget. Then run an --attune script:
jsreturn forget({ id: 'mem-id-to-delete', reason: 'Outdated by newer project guidance.', });
forget() archives the memory record by setting deleted_at and deleted_reason. It removes the record from active recall but does not delete the markdown file. Memory records survive index rebuilds and are never changed automatically.
Updating memories: updating memory is one user-approved operation: archive the old memory with forget(), then write and register a replacement markdown memory with remember(). If the user explicitly corrected the memory, that correction is approval for the combined archive-plus-write flow. If you discovered the mismatch yourself, ask first.
Search, then expand one promising hit:
jsconst hits = search('auth fix', { limit: 5 }); if (!hits.length) return []; return hits.slice(0, 3).map(h => ({ session_id: h.session.id, session_title: h.session.title, uuid: h.message.uuid, snippet: h.message.text?.slice(0, 240), }));
Check helper fields before assuming names:
jsconst rows = summaries({ project: '%quiet-zero%', limit: 1 }); return rows.length ? Object.keys(rows[0]) : [];
Fetch message neighbors without a full thread:
jsconst hit = search('runtime query', { limit: 1 })[0]; return sql( `SELECT uuid, role, timestamp, substr(text,1,240) AS snippet FROM messages WHERE session_id=? AND timestamp>=? AND COALESCE(visibility, 'visible') = 'visible' ORDER BY timestamp LIMIT 6`, hit.session.id, hit.message.timestamp );
See references/query-patterns.md for longer recipes.
~/.obelisk/obelisk.sqlite; old ~/.claude/obelisk.sqlite is copied forward if needed.raw(uuid, { offset, limit }) for specific JSONL windows.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 7,901 | 19,497 | +147% | 1 | 1 | 0% | 906 | 7,082 | +682% | 0 | 0 | — |
case-02 | fail→fail | 12,585 | 14,867 | +18% | 1 | 1 | 0% | 1,729 | 7,120 | +312% | 0 | 0 | — |
case-03 | fail→fail | 13,799 | 38,274 | +177% | 1 | 1 | 0% | 1,953 | 6,888 | +253% | 0 | 0 | — |
case-04 | fail→pass | 6,199 | 8,705 | +40% | 1 | 1 | 0% | 827 | 6,790 | +721% | 0 | 0 | — |
case-05 | pass→pass | 10,225 | 23,219 | +127% | 1 | 1 | 0% | 1,332 | 7,068 | +431% | 0 | 0 | — |
case-06 | fail→fail | 13,172 | 7,236 | -45% | 1 | 1 | 0% | 2,071 | 7,314 | +253% | 0 | 0 | — |
case-07 | fail→pass | 9,991 | 6,370 | -36% | 1 | 1 | 0% | 1,184 | 7,135 | +503% | 0 | 0 | — |
case-08 | fail→fail | 13,083 | 15,224 | +16% | 1 | 1 | 0% | 1,973 | 7,420 | +276% | 0 | 0 | — |
case-09 | pass→fail | 4,725 | 16,697 | +253% | 1 | 1 | 0% | 523 | 6,919 | +1223% | 0 | 0 | — |
case-10 | fail→pass | 12,373 | 3,277 | -74% | 1 | 1 | 0% | 1,618 | 6,577 | +306% | 0 | 0 | — |
case-11 | fail→fail | 7,494 | 12,053 | +61% | 1 | 1 | 0% | 1,227 | 7,078 | +477% | 0 | 0 | — |
case-12 | fail→fail | 24,958 | 18,805 | -25% | 1 | 1 | 0% | 1,775 | 7,865 | +343% | 0 | 0 | — |
case-13 | fail→pass | 8,207 | 7,007 | -15% | 1 | 1 | 0% | 1,067 | 7,457 | +599% | 0 | 0 | — |
case-14 | fail→fail | 11,138 | 6,567 | -41% | 1 | 1 | 0% | 1,538 | 7,336 | +377% | 0 | 0 | — |
case-15 | fail→pass | 9,049 | 5,184 | -43% | 1 | 1 | 0% | 1,229 | 6,900 | +461% | 0 | 0 | — |
case-16 | fail→pass | 6,605 | 8,827 | +34% | 1 | 1 | 0% | 951 | 7,833 | +724% | 0 | 0 | — |
case-17 | fail→pass | 17,009 | 3,149 | -81% | 1 | 1 | 0% | 2,449 | 6,665 | +172% | 0 | 0 | — |
case-18 | fail→fail | 14,719 | 4,728 | -68% | 1 | 1 | 0% | 2,220 | 6,932 | +212% | 0 | 0 | — |
case-19 | fail→pass | 13,156 | 6,136 | -53% | 1 | 1 | 0% | 1,990 | 7,406 | +272% | 0 | 0 | — |
case-20 | fail→pass | 7,860 | 3,900 | -50% | 1 | 1 | 0% | 1,165 | 6,799 | +484% | 0 | 0 | — |
case-21 | fail→pass | 10,460 | 6,509 | -38% | 1 | 1 | 0% | 1,578 | 7,267 | +361% | 0 | 0 | — |
case-22 | pass→fail | 6,458 | 16,052 | +149% | 1 | 1 | 0% | 865 | 7,345 | +749% | 0 | 0 | — |
case-23 | pass→pass | 6,315 | 15,958 | +153% | 1 | 1 | 0% | 921 | 7,095 | +670% | 0 | 0 | — |
case-24 | pass→pass | 8,090 | 6,079 | -25% | 1 | 1 | 0% | 1,226 | 7,273 | +493% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted, and 16 counted toward the lift figure. The other 8 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +33 percentage points is the difference between those two pass rates over the 16 comparable cases. 3 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 9/24/2026 | +41% |
Other measured skills in the registry, with their headline benchmark lift.