📚

Claude Codeに「昨日の続き」を思い出させるローカル知識ベースCLI「lk」を作った

に公開

はじめに

こんにちは。Androidエンジニアの@syarihuです。

普段からClaude Codeでコードを書いているのですが、使っているうちにずっと引っかかっていたことがありました。セッションが変わるたびに、前回さんざん読み解いたはずのモジュールを、また最初から読み直すことになる、という点です。同じファイルを開き、同じ関数を追いかけ、同じトークンを消費する。人間なら「あ、これ前に見たやつだ」で済むところを、毎回ゼロからやり直しているわけです。もったいないですよね。

そこで、Claude Codeのためのローカルな永続知識層があればいいのでは、と考えて作ったのが、今回紹介するlocal-knowledge-cli(コマンド名はlk)です。

作ってから使い込むうちに、もっと大きなジレンマにも効くことが分かってきました。コンテキストが積み重なってくると応答の精度が落ちてくるので、新しいセッションに切り替えて続きをやりたい。でも、標準のコンパクションでは情報が落ちすぎる。かといって、せっかく調べた内容を忘れてほしくはないし、必要なときにすぐ引き出せるようにしておきたい。いまのlkは、この「コンテキストをセッションを越えて使いたい」という場面で一番活躍しています。

lkとは

lkは、プロジェクトごとのローカルな知識ベースを管理するCLIです。Rust製・SQLiteバックエンドのシングルバイナリで、知識の検索・保存はネットワークアクセスなしのローカルで完結します(手元のリポジトリでは検索一発が数十ミリ秒です)。

調べた結果・設計判断・チームの規約などをローカルのSQLite(.knowledge/knowledge.db)に保存しておき、必要なときに全文検索で瞬時に引ける。そして、チームで共有したい知識はMarkdown(.knowledge/*.md)にエクスポートしてGitで追跡できる、というのが基本的な仕組みです。

先に言っておくと、lkは人間が手動でメモを管理するためのCLIというより、Claude Code自身が必要な知識を検索し、得られた知見を保存していくための外部記憶です。人間が毎回lk addを打つのではなく、Claudeが自律的に読み書きする使い方が基本になります。

主な機能を大きく分けると、次の3つです。

  • 外部記憶: プロジェクトローカル(.knowledge/knowledge.db)とユーザースコープ(グローバル)の知識ベース、トリグラムトークナイザによる日本語対応の全文検索、エントリ追加時の重複検出、APIキーなどを警告するシークレット検出
  • 共有と鮮度の管理: .knowledge/のMarkdownファイルとの双方向同期(Gitで共有可能、git pull後は自動同期)、設計判断のADR(Architecture Decision Record)記録、古くなったエントリのstale表示
  • Claude Codeとの連携: instructionsとスラッシュコマンドの同梱、MCP(Model Context Protocol)サーバー、Git worktree対応(すべてのworktreeで知識DBを自動共有)

ここで一番大事にしている考え方が、一時情報と共有知識を分けて保存する、という点です。調査ログやセッションのコンテキストのような一時的な情報は、高速にアクセスできるローカルDBへ気軽に放り込む。その中から実装経緯や設計思想のように、チームで共有したい知識が固まってきたら、Markdownに昇華してGitに載せる。この2層の設計が、後述する設計判断のあちこちに効いてきます。

もう1つ、はじめにでも触れたとおり、lkの用途は「同じコードの再調査を減らす」ことから「セッション間の引き継ぎ」へと広がってきました。会話の文脈を次のセッションへ持ち越すcontext保存や、設計をいったん保存して後日再開するplan機能はこの流れで入れたもので、こちらはモデルの性能にかかわらず価値があると感じています。

一方の共有知識の側には、設計判断をADRとして記録する機能があります。proposedからacceptedへというstatusの遷移や、方針転換で決定を置き換えたときのsupersedeリンクまで扱えるので、「なぜこうなっているのか」と「その後どう変わったのか」をセットで残せます。

ここまでに出てきた分類を整理すると、lkの知識には次の3つの軸があります。この記事でもこの言葉で説明していきます。

分類の軸 意味
スコープ project / user どのプロジェクトから参照できるか
保存形態 local / shared DBだけに置くか、Markdown+Gitで共有するか
カテゴリ context / plan / decisions など 何のための知識か

実際の動きを見てみる

仕組みの細かい話に入る前に、lk自身の開発リポジトリで実際に動かした例をお見せします。出力は読みやすさのためにキーワードなどを一部省略しています。

まずは、いまのlkが一番活躍している「セッション間の引き継ぎ」です。次のスクリーンショットは、前日の作業のあと、新しいセッションで「昨日の続きしたい」とだけ伝えたときの様子です。

「昨日の続きしたい」と伝えると、Claudeが知識ベースから前回のコンテキストを探しはじめる

こう伝えるだけで、Claudeはまずcontextカテゴリを検索して、前回の作業ログを探しにいきます。次の出力は、開発当時の引き継ぎメモを記事用にあらためて検索したものです(時間が経っているぶん、STALEの印が付いています)。

$ lk search "続き" --category context

 [34] NEXT: user-scope markdown export/sync の続き(前回ここで終了) (context) [STALE: 14 days since update]
       Keywords: user-scope, markdown, export, sync, next-steps, where-we-left-off, conversation-log,
       前回(このセッション)の続きの起点メモ。  完了済み(mainマージ済み): - PR#4: instructionsスリム化+skill化...

これはlkを開発していたときの実物のログで、「どこまで終わっていて、次に何をやるか」を書いた起点メモがそのまま返ってきます。前のセッションが何をどこまで進めたかをClaudeが把握した状態から会話が始まるので、「前回どこまでやったんだっけ」と思い出す時間がまるごと消えます。

こうして溜まった作業ログは、引き継ぎ以外にも使えます。たとえばcontextカテゴリを--sinceオプションで今日の分だけに絞って取り出せば、その日の作業ログから日報の下書きをClaudeに作らせる、といった使い方もできます。

もう1つは、調査の前の検索です。たとえば新しいセッションで「検索まわりの実装ってどうなってたっけ」と聞いたとします。Claude Codeはコードを読みはじめる前に、instructionsに従ってまず知識ベースを検索します。

$ lk search "search fts"

 [39] lk search ヒット率改善: FTS を OR 化 + 生クエリ回避指示(案A+C) (plan) [STALE: 13 days since update]
       Keywords: bm25, fts, fts5, like, recall, relevance, search, sqlite, trigram,
       ## 状態: proposed(設計確定・未着手)。別ブランチ/別PR推奨(plan-status PR #8 とは無関係)。  ## 背景・動機 lk の自動...

  [13] Search Implementation (features)
       Keywords: database, sqlite, schema, fts, full-text-search, search, keyword-fallback, search_entries, relevance-scoring, stale-detection
       The `search_entries()` function in `src/db.rs` implements a three-phase search: ...

過去に調査して保存してあった検索実装の解説エントリと、関連する改善計画がヒットしました。Claudeはこの内容を起点に回答をはじめるので、src/db.rsを頭から読み直す必要がありません。先頭にSTALEが付いているのは最終更新から時間が経っているという印で、instructionsには「staleなエントリは現行コードと突き合わせてから使う」という運用まで含めています。

インストールと基本的な使い方

インストールはHomebrewが一番手軽です。

brew install syarihu/tap/lk

シェルスクリプトやソースからのビルドにも対応しています。

# シェルスクリプト
curl -fsSL https://raw.githubusercontent.com/syarihu/local-knowledge-cli/main/setup.sh | bash

# ソースからビルド
cargo install --path .

使い方はシンプルです。プロジェクトでlk initして初期化し、あとは知識を足して検索するだけです。

# プロジェクトで知識ベースを初期化
cd your-project
lk init

# 知識を追加
lk add "API rate limit is 100 req/min" --keywords "api,rate-limit"

# 検索
lk search "rate limit"

# 全エントリを一覧
lk list

ちなみに、プロジェクトごとのlk initは必須ではありません。初期化していないプロジェクトでlk addすると、自動的にユーザースコープ(~/.config/lk/knowledge.dbのグローバルDB)へ保存されるので、どこでもそのまま使えます。検索も同様に、未初期化のプロジェクトではユーザースコープの結果を返してくれます。

Claude Codeと一緒に使うなら、instructionsをグローバルにインストールしておくのが手軽です。

# Claude Code向けのinstructionsを ~/.claude/ にインストール
lk init --global

これで、インストール直後からどのプロジェクトでも気軽に使いはじめられます。チームで知識を共有したくなったプロジェクトにだけ、あらためてlk initすればよい、という運用です。

日々のインフラとして常駐させておいて、lk updateで自分自身を最新版に更新できるようにもしています。

git worktreeを使っている場合は、すべてのworktreeがメインworktreeの知識DBを自動的に共有します(設定は不要です)。worktreeを分けてClaude Codeを並列に走らせていても、どこかのセッションが保存した知識をすぐ他のworktreeから引けます。

Claude Codeとの連携

lkはClaude Codeと連携するための経路を2つ用意しています。

instructionsとスラッシュコマンド

基本の経路はinstructionsファイルです。lk initを実行すると、.knowledge/lk-instructions.mdというClaude Code向けの指示ファイルが作られ、AGENTS.md(またはCLAUDE.md)に@構文でインポートされます。これによって、設定ファイル本体は最小限に保ったまま、フルの指示をClaude Codeに渡せます。

初期化のあと、Claude Codeは次のように動いてくれます。

  1. 見慣れないコードを調べる前に、まずlk searchで知識ベースを検索する
  2. 調査で得た新しい発見をlk addで保存する(重複があれば追加ではなく更新する)
  3. 設計判断やセッションの文脈も、区切りのタイミングで自律的に保存する

ポイントは、Claudeがinstructionsに従ってlkコマンドを直接実行する、という点です。特別な連携の仕組みがなくても、指示とCLIだけでこの自律的なループが回ります。

/lk-knowledge-refresh(古くなったエントリの一括更新)や/lk-knowledge-plan(あとでやる計画の保存・再開)といったスラッシュコマンドも同梱していますが、こちらは日々のループというより、ユーザーが明示的に呼び出すワークフロー向けのものです。

私が普段いちばんよく使うのは、こうしてユーザーから明示的に一声かける使い方です。よくあるのは2つで、1つはセッションの引き継ぎです。planやcontextはClaudeが区切りのタイミングで自動的に保存してくれることもありますが、その日の作業をいったん区切りたいときや、作業の途中で別のセッションに引き継ぎたいときには、こちらから明示的に頼みます。「今日やったことをcontextに保存しておいて」と伝えるか、/lk-knowledge-save-contextを直接呼んでおくと、翌日は冒頭で紹介した「昨日の続きしたい」だけで、保存したログから再開できます。

もう1つは、あるタスクを進めている途中で派生タスクや別で調べたいことに気付いたときです。手を止めてそちらに取りかかると本来の作業が中断してしまうので、「それlkに保存しといて」とだけ伝えて、あとでやる計画やメモとして残してもらいます。目の前の作業に集中したまま、みつけたタスクを取りこぼさずに済みます。

MCPサーバー(オプション)

もう1つの経路がMCPサーバーです。登録すると、Claudeが会話中にMCPツール経由で知識を検索・追加・管理できるようになります。

# Claude Code / Claude Desktop の両方に登録(現在のプロジェクトを自動検出)
cd your-project
lk install-mcp

登録すると、Claudeはsearch_knowledge(検索)、add_knowledge(追加)、get_knowledge(取得)、update_knowledge(更新)といったツールを使えるようになります。サーバーの手動起動は不要で、ツールが呼ばれたときにClaude Code / Claude Desktopが自動的にlk mcpを立ち上げます。なお、MCP経由の場合は応答に最新版の有無を含めるため、1日1回だけGitHubへ更新チェックの通信が入ります。

実は、MCP対応を入れた当初は、instructionsに書くよりもMCPツールとして見えているほうが「ここで検索すべき」と気付いてもらいやすい感覚がありました。ただ、モデルやClaude Code自体の進化に加えて、instructionsやスキルを改善していった結果、いまはMCPがなくてもちゃんと呼ばれるようになりました。実際、私自身も現在はMCPなしで運用しているので、まずはinstructions経路で使いはじめて、必要になったら足すくらいの位置付けでよいと思います。

設計の話

ここからは、作っていく中で悩んで決めた設計の話をいくつか紹介します。エンジニアの方に刺さりそうなところを選びました。

sharedはMarkdownが正、SQLiteは検索インデックス

lkの知識には「shared」と「local」の2種類があります。

  • shared.knowledge/のMarkdown、Git追跡)— アーキテクチャ、設計判断、チーム規約など、チーム全体で知っておきたい安定した知識
  • local(DBのみ、Git無視)— LLMの調査キャッシュ。似たタスクを繰り返すときのコンテキスト消費を減らすための、使い捨てのキャッシュ

このとき「安定した事実はshared(Markdown + Git)、使い捨てキャッシュはlocal(DB)」という線引きを最初に決めました。localに溜めた調査結果のうちチームに残したいものは、lk exportでMarkdownへ書き出して共有知識に昇華できます。ここから自然に導かれるのが、sharedな知識は「Markdownを正とする」というモデルです。SQLiteはあくまで再構築できる検索インデックス(git-ignore)で、sharedの正はいつでも.knowledge/*.md側にある。一方、使い捨てであるlocalな知識はDBだけに保存します。だからgit pullしたあとは、ファイルハッシュの差分を見て自動同期がかかり、手動でのlk syncは要りません。

検索は取りこぼさない3段構え

知識ベースというのは、保存より検索のほうが本質です。せっかく保存しても、引っかからなければないのと同じです。lkの検索はrecall(再現率)優先という思想で、3段構えにしています。

  1. FTS5のトリグラム全文検索
  2. キーワードテーブルによる補完
  3. 短いCJK語向けのLIKEフォールバック

このあたりは実際に痛い目を見て直したところでもあります。以前は複数単語のクエリがほぼ0件になるバグがありました。原因は、クエリを空白で連結するとFTS5では全語ANDになってしまい、少しでも語がずれると一気にヒットしなくなる、というものでした。そこでANDマッチからORマッチに変更し、いずれかの語を含むエントリを拾ったうえで、より多く・よりレアな語にヒットしたものをbm25で上位に並べるようにしました。ゆるく書いたクエリでも、一番関係ありそうなエントリが浮かんでくるようになっています。

project/userの2スコープと、UIDによるアドレッシング

lkにはプロジェクトごとの知識ベースのほかに、ユーザースコープ(グローバル)の知識ベース(~/.config/lk/knowledge.db)があります。個人メモや、セッションの引き継ぎログのような「プロジェクトをまたいだ文脈」を全プロジェクトで持ち回るためのものです。

これを入れるときに悩んだのが、IDの衝突問題でした。projectのDBもuserのDBも、それぞれid=1から採番するので、両方を混ぜて読むと当然ぶつかります。--scopeで読み分ける案やu3のような接頭辞を付ける案も考えたのですが、どれも扱いが煩雑になるので却下し、最終的にグローバルで一意な12文字のhex UIDでアドレッシングすることにしました。数値IDは後方互換のためprojectスコープ専用として温存しています。UID生成では、a〜fの英字を必ず1文字含めるようにして、数値IDと誤認されないようにする、という細かい工夫も入れています。

ユーザースコープは最初はDBのみに絞って出し、Markdownのexport/syncは段階的に追加しました。今ではdotfilesリポジトリを指すように設定して、個人ナレッジを複数マシンで同期できるようになっています。

# ユーザーDBをMarkdownにエクスポート(md側が正になり、エントリはsharedに変わる)
lk export --scope user

# 別マシンで git pull したあと、MarkdownをユーザーDBに取り込む
lk sync --scope user

projectスコープのsharedと違って、ユーザースコープのMarkdownはプロジェクトの外にあるため自動同期の対象外です。別マシンで取り込むときは、明示的にlk sync --scope userを実行します。エクスポート先はdotfilesリポジトリに向けられるので、個人の知識も普段のdotfilesと同じ感覚で持ち歩けます。

保存は「高密度」であって「長い」ではない

知識を保存するのは基本的に人間ではなくClaudeなので、これは「Claudeにどう保存させるか」という設計の話です。instructionsには、長々とした経緯ではなく、決定の理由・捨てた案・関数名やパスといった「消えると困る具体物」を密度高く残すように指示しています。キーワードも自動抽出に任せきりにせず、5〜10個に厳選したものを渡させています。

理由はシンプルで、肥大化したエントリはrecall時にコンテキストを食うからです。せっかく「Claudeがコードを読み直さずに済むように」と作った知識ベースなのに、エントリが長すぎて呼び出すたびに大量のコンテキストを消費してしまっては、守りたかったものを浪費していることになります。キーワードの上限を15個にしているのも、同じ考え方からきています。

古くなった知識は「古い」と明示する

コードは変わり続けるので、保存した知識は放っておくと現実とズレていきます。lkはこれを前提に、最終更新からの経過日数で古さを判定します。sharedは30日、localは7日(どちらも.knowledge/config.tomlで変更可能)更新がないと、先ほどの検索例にもあったように、検索結果に[STALE: N days since update]という印が付きます。

大事にしているのは、staleでも検索結果から消さないことです。古くなった知識にも手がかりとしての価値はあるので、「古い」と明示したうえで返し、扱い方はinstructions側に寄せています。staleなエントリは現行コードと突き合わせてから使い、まだ正しければlk edit --touchで確認済みとして更新日時だけリセットする。内容が古くなっていれば更新し、localの調査キャッシュならパッチせずに調べ直す。まとめて棚卸ししたいときは/lk-knowledge-refreshで一括チェックもできます。

AIに使われることを前提にする

lkはAIに使わせることを前提にしたツールなので、AIがどれだけ自律的にlkを使ってくれるかが価値に直結します。その自律性を左右するレバーは、MCPを使う場合はツールのdescription、instructions経路ならトリガーの書き方です。ここに何をどう書くかで、Claudeが「ここは検索すべき場面だ」と判断してくれるかどうかが変わります。

同じ発想で、毎ターン読み込まれるlk-instructions.mdはできるだけ薄くしました。最初は127行あったのですが、トリガー層(「こういうときに動く」という引き金の部分)だけを残して29行まで削り、詳細はスキル側に追い出しています。毎ターン積まれるものを軽くしておくのは、まさにlk自身が解決しようとしているコンテキスト浪費の問題そのものなので、ここは徹底しています。

lkの開発にlkを使う

このプロジェクトでは、lkの開発そのものにlkを使い倒しています。自分で使いたかったのはもちろんですが、lkの活用例としては、lk自身の開発で使ってみせるのが一番分かりやすいだろう、という狙いもあります。

lk自身の設計判断や開発ログは、そのままlkの知識ベースに保存されています。ADRが2件、planが4件、contextが10件以上、sharedが21件。この記事で紹介している設計判断のいくつかも、実は当時lkに保存したエントリから引っ張ってきています。

この記事の素材集め自体、知識ベースに残っていたADR・context・planを掘り起こすところから始まりました。執筆中に出てきた「コードからは読み取れない経緯」(MCPの位置付けの変遷など)は、記事に反映すると同時にlkへ保存し直しています。仮にここで執筆が中断しても、次のセッションは保存した知識から再開できます。

素材調査そのものはサブエージェントに委譲したのですが、このときの運用もinstructionsに組み込んであります。「調査の前にまずlk searchする」「発見は## Knowledge to Saveという節で返す」というブリーフを付けて投げ、返ってきた知見を親のセッションが知識ベースへ保存する、という流れです。委譲した調査の成果も、こぼさず知識ベースに積み上がっていきます。

特にメタだったのがplan機能です。「あとでやる計画を保存して、あとから再開できるようにする」という機能なのですが、そのplan機能の実装計画それ自体を、実装前のplan機能で保存したのです。実装が終わってから/lk-knowledge-planで一覧を開くと、リストのトップに「この機能の実装」という計画が並んでいる、という妙な光景になりました。自分で自分を作るときに自分を使う、みたいな話で、書いていて少し楽しかったところです。

planは今もカジュアルに増え続けています。実はこの記事の執筆中にも、事実確認でREADMEのUIDに関する記述が実装より古いことに気付いたのですが、執筆の手を止めてREADMEを直すのではなく、「READMEの記述を修正する」という計画をstatus: proposedで保存して後回しにしました。目の前の作業に集中しつつ、みつけたタスクは失われない。これも引き継ぎのための一時保存です。

ADRについても専用コマンドは作らず、status・UID・双方向のsupersede・カテゴリごとのテンプレートといった汎用の拡張だけで実装しました。汎用にしておいたおかげで、ADRだけでなく、Google Cloudが公開しているOpen Knowledge Format(OKF)のような他のフォーマットにも対応しやすい作りになっています。

ユーザースコープのDBには、いつのまにか他プロジェクトの作業ログが50件以上たまっていて、今では日常のインフラとして完全に実稼働しています。自分で使っていて「これはないと困る」状態になったのは、作ってよかったなと思える瞬間でした。

まとめ

lkは、Claude Codeが同じコードを何度も調べ直してトークンを浪費する、という課題から生まれて、そこからセッション間の引き継ぎへと用途の幅を広げてきたツールです。

ローカル完結・高速・シングルバイナリで動き、共有知識はMarkdownを正としつつSQLiteで瞬時に検索でき、instructionsとスラッシュコマンド(必要ならMCPも)でClaude Codeと連携する。そして「安定した事実はチームで共有、使い捨てキャッシュは手元に」という割り切りのもとで、知識を密度高く保存し、セッションを越えて引き継いでいく。そんな道具になっています。

コンテキストが長くなってきたから新しいセッションに切り替えたいけれど、自動コンパクションだと文脈の精度に不安がある。今日の作業ログを保存しておいて、明日は「昨日の続きから」で始めたい。個人開発で、Issueを立てるほどではない計画をとりあえず置いておいて、あとで選んで実装したい。そんなふうに「コンテキストをセッションを越えて使いたい」と感じたことがある方は、ぜひ一度試してみてください。

Discussion