Obsidian Web Clipperの代わりにクリップボードからノートを作るzsh関数

はじめに

Obsidian はローカルの Markdown ファイル群をノートとして扱うエディタで、公式の Web Clipper ブラウザ拡張を使うと、見ているページのタイトル・URL・本文を所定のテンプレート(frontmatter 付き Markdown)として Vault に保存できる。

ただ「ブラウザ拡張を開いて保存」という導線が、ターミナル中心の作業だと少し重い。コピーした文章をターミナルから一発でノート化したい。この記事では、Web Clipper と同じ出力フォーマットを zsh 関数で再現する過程をまとめる。題材は投資ニュースを raw/news/ に貯める用途だが、保存先とテンプレートを差し替えれば汎用的に使える。

ゴールはこうだ:

  • 既定はクリップボードから本文を取得して即保存(ブラウザでコピー → ターミナルでコマンド実行だけ)
  • -p を渡したときだけ、ターミナルへの貼り付けを待ち受ける
  • パイプ(pbpaste | clip-news)でも動く
  • 出力は Web Clipper のテンプレート(type: clip / status: inbox などの frontmatter)と一致させる

出力したいフォーマット

Web Clipper 側のテンプレートに合わせ、こういうノートを YYYY-MM-DD-タイトル.md で生成する。

---
type: clip
status: inbox
title: "(ファイル名から)"
source: ""
clipped: "2026-06-10"
ticker:
theme:
---

> [!info] raw clip — 直接編集せず、後で要約・統合する。

(本文)

ファイル名は入力で指定でき、空 Enter なら 2026-06-10-ab3xy.md のようにランダムな5文字を付ける。

ハマりどころ: 複数行ペーストと read

最初の素朴な実装は「本文を1行 read する」だったが、これは複数段落の記事で破綻する。

ターミナルの行入力(cooked mode)では、ペーストした文字列に含まれる改行がそれぞれ確定として扱われる。read 1回では1行目しか取れず、残りの行はシェルのプロンプトに流れ込み、最悪そのままコマンドとして実行される。これは地味に危険だ。

そこで「最初の1行は待つ → 残りはまとめて吸い出す」方針にする。最初の版ではタイムアウト付き read で残りをドレインした。

IFS= read -r line; body="$line"
while IFS= read -r -t 0.4 line; do   # 入力が 0.4 秒止まったら確定
  body+=$'\n'"$line"
done

ペーストは一気に届くので、0.4 秒の猶予で2行目以降を回収でき、溢れも防げる。ただし末尾で必ず 0.4 秒待つのが気になる。

read -t 0 で待ち時間をゼロにする

「Enter を押した瞬間に保存したい(末尾の待ちを消したい)」という要望に対し、zsh の read -t 0 が効いた。

zsh のドキュメント上 -t 0 は「入力があるか判定するだけ(消費しない)」と読めるが、手元の zsh 5.9 で試すと 入力があれば即座に1行読み、なければ即座に偽を返す挙動だった。つまりこれをドレインのループにそのまま使える。

IFS= read -r line; body="$line"
while IFS= read -r -t 0 line; do   # 残りを待たずに吸い出す
  body+=$'\n'"$line"
done
  • 単一行: 残りが無いのでループは即終了 → 待ち時間ゼロ
  • 複数行: ペーストはまとめて届いているので -t 0 が次々に回収

トレードオフとして、-t 0 は「今この瞬間に入力があるか」で判断するため、巨大なペーストが複数チャンクに分割して届くとごく稀に末尾を取りこぼす。ニュースのスニペット程度なら一括で届くので実用上は問題ない。長文は後述のパイプを使えば確実だ。

3つの入力モードを分岐する

最終形は「既定=クリップボード / -p=待ち受け / パイプ=stdin」の3経路にした。判定は [[ -t 0 ]](stdin が端末か)と -p フラグの2つだけ。

clip-news() {
  emulate -L zsh
  local VAULT="$HOME/path/to/vault"
  local DIR="$VAULT/raw/news"
  local DATE; DATE="$(date +%Y-%m-%d)"
  mkdir -p "$DIR"

  local wait_input=0
  [[ "$1" == "-p" ]] && wait_input=1

  local name="" body="" line
  if [[ ! -t 0 ]]; then
    body="$(cat)"                          # パイプ/リダイレクト: stdin 全部
  else
    printf 'ファイル名(空Enter で自動: %s-XXXXX): ' "$DATE" >&2
    IFS= read -r name
    if (( wait_input )); then              # -p: 貼り付けを待ち受け
      printf '本文を貼り付けて Enter:\n' >&2
      if IFS= read -r line; then
        body="$line"
        while IFS= read -r -t 0 line; do
          body+=$'\n'"$line"
        done
      fi
    else
      body="$(pbpaste)"                     # 既定: クリップボードから取得
    fi
  fi

  local base title
  if [[ -z "${name//[[:space:]]/}" ]]; then
    base="${DATE}-$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c 5)"
    title=""
  else
    base="${DATE}-${name//\//-}"            # '/' はファイル名に使えないので '-'
    title="${name//\"/\\\"}"               # YAML 用に '"' をエスケープ
  fi

  local file="$DIR/${base}.md" i=2
  while [[ -e "$file" ]]; do                # 同名は連番で回避
    file="$DIR/${base} ${i}.md"; ((i++))
  done

  {
    print -r -- "---"
    print -r -- "type: clip"
    print -r -- "status: inbox"
    print -r -- "title: \"${title}\""
    print -r -- "source: \"\""
    print -r -- "clipped: \"${DATE}\""
    print -r -- "ticker:"
    print -r -- "theme:"
    print -r -- "---"
    print -r --
    print -r -- "> [!info] raw clip — 直接編集せず、後で要約・統合する。"
    print -r --
    print -r -- "$body"
  } > "$file"

  print -r -- "✓ 保存: ${file#$VAULT/}" >&2
}

ポイント:

  • プロンプトやメッセージは >&2(標準エラー)へ。標準出力をパイプで汚さないため。
  • pbpaste は macOS のクリップボード読み出し。Linux なら xclip -o 等に置き換える。
  • ランダム5文字は tr -dc 'a-z0-9' < /dev/urandom | head -c 5。
  • emulate -L zsh で関数内だけ素の zsh 挙動に固定し、ユーザー環境の setopt に左右されないようにする。

スクリプトではなく関数で、dotfilesに置く

当初は Vault 内に .zshrc から呼ぶ外部スクリプトを置いていたが、最終的に dotfiles リポジトリの .zshrc に関数として直書きする形にした。~/.zshrc が dotfiles 内のファイルへの symlink になっているため、編集対象は実体である dotfiles 側になる。こうすると外部ファイル依存がなくなり、シェル設定がバージョン管理下で self-contained になる。

~/.zshrc -> ~/ghq/github.com/<user>/dotfiles/.zshrc   # symlink

まとめ

  • Obsidian Web Clipper の出力(frontmatter 付きノート)は、テンプレートを固定すれば zsh 関数で十分再現できる。
  • ターミナルでの複数行ペーストは read 1回だと取りこぼし&プロンプトへの溢れが起きる。read -t <timeout> で残りをドレインするのが定石。
  • 末尾の待ち時間が嫌なら read -t 0 が使える(手元の zsh では「あれば即読み・無ければ即終了」)。ただし巨大ペーストの分割到着には弱いので、確実さが要るならパイプ経由にする。
  • 入力経路は [[ -t 0 ]] と1つのフラグで「クリップボード / 待ち受け / パイプ」に素直に分岐できる。
  • 単発スクリプト+alias より、dotfiles の .zshrc に関数として置くほうが依存が減って管理しやすい。

AIエージェントの長期記憶とタスク管理: EngramとBeadsの設計・使い分け

はじめに

AI コーディングエージェントは、セッションをまたぐと設計判断や調査結果を忘れる。毎回同じ背景を説明し直すのは手間だが、会話ログを無差別に外部サービスへ預けるのも避けたい。

Engram は、Codex、Claude Code、OpenCode、Gemini CLI などのエージェントに長期記憶を与えるローカルファーストのCLIだ。一方、Beads は、依存関係を持つ作業をエージェントと人で進めるための分散Issue Trackerだ。この記事では、両者の設計、処理フロー、CloudなしのEngram運用、使い分けを整理する。

Engramが解決すること

Engramは会話全文をそのまま記録する仕組みではない。エージェントが作業中に得た「あとで再利用できる知見」を、プロジェクト単位の記憶として保存する。

例えば、設計判断、バグの根本原因、修正の注意点、運用手順を What / Why / Where / Learned のような構造で残す。次のセッションでは、エージェントが関連する記憶だけを検索して読み戻す。

この方式の利点は、プロンプトに全履歴を毎回詰め込まずに、長期的な文脈だけを維持できることだ。

アーキテクチャ

基本構成はシンプルだ。

Codex / Claude Code / OpenCode / Gemini CLI
        │
        │ MCP(標準入出力)
        ▼
engram mcp
  ├─ mem_save      知見を保存
  ├─ mem_search    関連知見を検索
  ├─ mem_context   セッション開始時の文脈を取得
  └─ mem_doctor    状態を診断
        │
        ▼
SQLite + FTS5 全文検索
~/.engram/engram.db

MCPはHTTPサーバーではなく標準入出力で接続する。Codexの場合、起動時に engram mcp --tools=agent が子プロセスとして立ち上がり、エージェントは mem_* ツールを通じてローカルDBへアクセスする。

保存先はSQLiteで、FTS5による全文検索を使う。記憶を保存する主体はEngramではなくエージェントである点も重要だ。Engramは「何が重要か」を推測して収集するより、エージェントが確定した知見を明示的に残す前提で設計されている。

処理の流れ

  1. engram setup codex がCodexのMCP設定と、記憶利用・圧縮復元の指示ファイルを追加する。
  2. セッション開始時、エージェントは mem_context や mem_search で関連する過去の知見を取得する。
  3. 実装や調査の節目で、エージェントが mem_save を呼び、知見をSQLiteへ保存する。
  4. 次のタスクでは、検索結果のうち関連度が高いものだけがエージェントのコンテキストへ渡る。
  5. 必要になった場合だけ、ローカル同期またはCloud同期を使う。

コンテキスト圧縮後にも保存・検索の手順を促す指示を使うため、長い作業で会話が要約されても記憶の運用を継続しやすい。

Beadsが解決すること

Beadsの主目的は記憶そのものではなく、依存関係つきの作業管理だ。IssueをBeadとして保存し、優先度、親子関係、ブロッカー、担当、状態をグラフとして扱う。

bd create
  │ 作業を作成する
  ▼
依存関係グラフ
  │ bd dep add でブロッカーを表現する
  ▼
bd ready
  │ 着手可能な作業を取り出す
  ▼
bd update <id> --claim
  │ 担当を原子的に取得する
  ▼
実装・レビュー
  ▼
bd close
  │ 完了し、依存先の作業を解放する

エージェントは bd prime でワークフローとプロジェクト記憶を取得し、bd ready で次に着手すべき作業を選ぶ。bd remember で簡潔なプロジェクト記憶も保存できるが、中心にあるのは「何を、どの順序で、誰が進めるか」という状態管理だ。

Beadsの保存と同期

BeadsはDoltを使う。Doltは履歴、ブランチ、マージを扱えるSQLデータベースで、デフォルトの埋め込みモードではプロジェクト内の .beads/embeddeddolt/ にデータを保存する。

ローカル .beads/ のDolt DB
       │
       ├─ bd dolt push
       ▼
Git remote の refs/dolt/data
       ▲
       └─ bd dolt pull
他の端末・他のエージェント

.beads/issues.jsonl は閲覧・交換用のエクスポートであり、正本ではない。通常は埋め込みモードの単一ライターで使い、複数の同時書き込みが必要になった場合は外部の dolt sql-server を使うサーバーモードへ移行する。

Gitを触れない共有リポジトリや個人用の作業には、bd init --stealth と BEADS_DIR を使うローカル専用モードもある。

EngramとBeadsの比較

観点EngramBeads
主目的設計判断や学びの長期記憶依存関係つきの作業管理
保存単位決定、原因、注意点、運用知識Issue、Epic、Sub-task、依存、担当、状態
中心操作mem_save / mem_search / mem_contextbd create / bd ready / bd update --claim / bd close
データモデルSQLite + FTS5全文検索Dolt上のグラフ型Issueデータ
次の行動関連する知識を返すブロッカーのない着手可能な作業を返す
共有ローカル、.engram/、任意のCloudDolt remoteへのpush/pull、またはDoltサーバー

要するに、Engramは「なぜそうするのかを覚える」仕組みで、Beadsは「次に何を、誰が、どの順序で行うかを管理する」仕組みだ。

併用時の役割分担

両者は競合するより補完し合う。情報を二重管理しないために、保存先を次のように決めるとよい。

Beads
  - Issue、優先度、依存関係、担当、完了条件
  - 作業を開始・終了する事実

Engram
  - 設計判断の理由
  - 調査で分かった制約
  - 再発しやすい障害の原因と対処
  - 完了後にも再利用したい学び

例えば「認証リファクタリング」はBeadsのEpicとTaskで管理し、「refresh tokenを平文保存しない理由と採用方式」はEngramに保存する。

導入時には設定の重なりに注意する。Beadsの bd init はプロジェクトの AGENTS.md を更新し、EngramはCodex向けのMCP設定と指示ファイルを追加する。併用する場合は既存の設定を上書きせず、両方のワークフローが残ることを差分で確認する。

Cloudは必須ではない

Cloudは初期状態で有効ではない。Cloudを設定しなければ、記憶はローカルの ~/.engram/ にだけ残る。

通常の engram sync はプロジェクト内の .engram/ へのローカル同期であり、Gitで共有する用途に使える。複数端末やチームで中央共有したい場合にだけ、明示的なCloud設定と engram sync --cloud を使う。

Cloudを使わない方針なら、次を守ればよい。

  • engram cloud enroll を実行しない
  • engram sync --cloud を実行しない
  • ENGRAM_CLOUD_SERVER と ENGRAM_CLOUD_TOKEN を設定しない
  • Cloud向けの自動同期を有効にしない

Cloudを使う場合は、同期対象の記憶を機密情報として扱う。自前ホスト、TLS、強い別々のトークン、プロジェクト許可リストを前提にしたい。

Codexでの導入

macOSでCodexを使うなら、まずローカル専用で導入するのが扱いやすい。

umask 077
brew install gentleman-programming/tap/engram
engram setup codex
engram doctor
chmod 700 ~/.engram

セットアップ後はCodexを再起動する。engram setup codex はMCP登録だけでなく、~/.codex/ に記憶プロトコルとコンテキスト圧縮用の指示ファイルも作成する。

便利さを最大化するならこの自動セットアップがよい。一方で、厳格な環境ではMCP設定だけを手作業で追加し、プラグイン導入はコード確認後に分ける選択肢もある。

漏洩リスクをどう見るか

ローカルファーストは重要な利点だが、情報が外部へ出る経路が完全にゼロになるわけではない。

経路リスク対策
エージェントへの検索結果mem_search の結果はエージェントのモデルコンテキストに入る。以降は利用するAIサービスのデータポリシーにも従うAPI鍵、秘密鍵、認証情報、個人情報を保存しない
ローカルDB~/.engram/ に記憶が残る。端末の他ユーザーやバックアップの閲覧権限が問題になるumask 077 と chmod 700 ~/.engram を使う
ローカルHTTP APIengram serve はループバックに限定されるが、同じMacのプロセスからは到達できる常駐させるなら ENGRAM_HTTP_TOKEN を設定し、不要なら起動しない
Cloud同期記憶チャンクが設定したCloudサーバーへ送られるCloudを不要な限り無効にし、自前ホストではTLSと強いトークンを使う
更新確認GitHub Releases APIへの更新確認が走る。環境に GH_TOKEN か GITHUB_TOKEN があればGitHubへBearerトークンを送る更新確認用の実行環境に不要なトークンを渡さない
プラグイン導入Codexセットアップはmarketplaceからプラグインを導入する高保証環境では導入前に内容を確認し、追従ポリシーを決める

重要なのは、「Engram自身の外部通信」と「Engramが取り出した記憶をAIモデルに渡すこと」を分けて考えることだ。Cloudを無効にしても、検索した記憶をホスト型モデルへ送るなら、その内容はモデル側の取り扱い対象になる。

確認した範囲

この時点のソースコードに対して go vet ./... は成功し、Obsidianプラグインの本番依存に対する npm audit --omit=dev は脆弱性0件だった。

ただし、これは侵入テストやCloud運用環境の監査ではない。Go依存の既知CVEを照合する govulncheck は実行環境に入っていなかったため、依存関係に問題がないと断定するものでもない。機密性が高い用途では、導入するバージョンを固定し、独立した脆弱性スキャンと運用設定のレビューを行いたい。

まとめ

Engramは、AIエージェントの重要な知見をSQLiteへ残し、必要な時だけMCPで取り出すローカルファーストの記憶基盤だ。Beadsは、依存関係をもつ作業をDolt上で管理し、次に進める作業を明確にする。Cloudは必須ではなく、EngramはCodexとのローカルMCP連携だけでも長い作業の文脈維持に役立つ。

個人の知見を長く再利用したいならEngramから、複数エージェントや複数ブランチで並行開発するならBeadsから始めるのが自然だ。併用する場合は、Beadsに作業の事実を、Engramに判断の背景を残す。長期記憶とタスク管理の便利さは、保存対象とアクセス境界を意識して初めて安全に活かせる。

deepsecのscan/processとLocal LLM運用を整理する

はじめに

deepsec は、AI agent を使って既存のコードベースから脆弱性を探すためのツールである。単純な静的解析ツールというより、まずローカルで怪しい箇所を広く拾い、その後に LLM へ調査させる二段構えのセキュリティレビュー基盤に近い。

この記事では、deepsec を読みながら確認した内容をもとに、重要な機能、scan と process の違い、コードが外部に送られるタイミング、Local LLM や LiteLLM を使った閉域運用の可能性を整理する。

deepsecの中心機能

deepsec の核は、以下のようなパイプラインである。

scan -> process -> revalidate -> enrich -> report/export/metrics

scan は、コードベースをローカルで走査し、regex matcher によって「調査候補」を見つける。process は、その候補を LLM に読ませて、本当に脆弱性として成立するかを調査する。revalidate は既存の finding を再検証し、false positive や修正済みを判定する。enrich は git 履歴や ownership 情報を付与する。最後に report や export で人間や CI が扱いやすい形に整える。

対応範囲は特定の言語に限定されていない。JavaScript / TypeScript、Python、Go、PHP、Ruby、Terraform、Dockerfile、Kubernetes、GitHub Actions、AI agent 関連のコードなどを対象にしている。ただし、言語や framework ごとに matcher の厚みには差がある。Next.js や JS/TS、Python、Go、Rails、Laravel などは比較的手厚く、Rust、JVM、.NET は tech detection はあるものの、専用 matcher はまだ発展途上という印象だ。

また、deepsec は plugin 機構を持っている。built-in matcher だけで足りない場合は、社内 framework や独自 API のパターンに合わせて matcher や agent backend を追加できる。

scanとprocessの違い

scan と process は混同しやすいが、役割はかなり違う。

scan はローカルで動く機械的な候補抽出である。ファイルを glob し、tech detection を行い、matcher を走らせて candidates を作る。この時点では脆弱性と確定していない。

例えば、以下のようなコードがあるとする。

export async function GET(req: Request) {
  const id = new URL(req.url).searchParams.get("id");
  const user = await db.query(`SELECT * FROM users WHERE id = ${id}`);
  return Response.json(user);
}

scan はこのコードを見て、Next.js の route handler らしい、raw SQL らしい、外部入力が絡んでいるかもしれない、といった候補を記録する。

{
  "filePath": "app/api/users/route.ts",
  "candidates": [
    {
      "vulnSlug": "js-sql-raw",
      "lineNumbers": [3],
      "matchedPattern": "raw SQL string interpolation"
    },
    {
      "vulnSlug": "all-route-handlers",
      "lineNumbers": [1],
      "matchedPattern": "Next.js route handler"
    }
  ],
  "status": "pending"
}

一方、process は LLM による実調査である。scan が作った候補ファイルを batch にし、Codex や Claude などの agent backend に読ませる。LLM は、入力が本当に attacker-controlled か、認証や validation があるか、ORM が parameterize していないか、脆弱性として実害があるかを判断する。

本当に問題があると判断されると、candidates から findings に昇格する。

{
  "filePath": "app/api/users/route.ts",
  "findings": [
    {
      "severity": "HIGH",
      "vulnSlug": "js-sql-raw",
      "title": "SQL injection in user lookup route",
      "description": "The id query parameter is interpolated directly into SQL.",
      "lineNumbers": [3],
      "recommendation": "Use a parameterized query instead.",
      "confidence": "high"
    }
  ],
  "status": "analyzed"
}

逆に、scan が候補にしても、process が安全だと判断すれば finding は出ない。例えば次のように parameterized query になっていれば、候補には残っても、最終 finding にはならない可能性が高い。

await db.query("SELECT * FROM users WHERE id = ?", [id]);

つまり、scan は「安く広く拾う」段階であり、process は「高コストだが深く判断する」段階である。

データは外部に送られるのか

通常の deepsec scan 単体では、ソースコードや候補データは外部に送られない。実装上は、ローカルファイルを読み、tech detection と regex matcher を実行し、結果を data/<projectId>/ 配下に書くだけである。

一方で、以下の操作では外部送信が発生する。

  • process: 候補ファイルや repo context を LLM provider に送る
  • revalidate: 既存 finding と関連コードを LLM provider に送る
  • triage: finding の説明や project context を Claude に送る
  • sandbox scan / sandbox process: Vercel Sandbox に repo や data の tarball をアップロードする
  • plugin: 外部通信する plugin を読み込んだ場合、その plugin 次第で送信される

したがって、閉域で運用したい場合に重要なのは、scan ではなく process、revalidate、triage をどう扱うかである。

Local LLMで外部送信を避けられるか

Local LLM を使えば、process や revalidate でコードを外部 provider に送らない構成は可能である。ただし、現状の deepsec にそのまま差し替えられるかは、Local LLM の API 互換性に依存する。

一番近い経路は --agent codex である。Codex backend は OPENAI_BASE_URL と OPENAI_API_KEY を見て、OpenAI 互換 endpoint に向けられる。ただし、ここで要求されるのは一般的な /v1/chat/completions ではなく、OpenAI Responses API の /v1/responses である。

そのため、Local LLM サーバが Responses API に十分対応していれば、以下のような形で試せる。

OPENAI_API_KEY=local
OPENAI_BASE_URL=http://127.0.0.1:8000/v1
pnpm deepsec process --project-id <id> --agent codex --model <local-model-name>

しかし、多くのローカル OpenAI-compatible server は /v1/chat/completions だけを実装している。その場合、環境変数を差し替えるだけではうまく動かない可能性がある。

より堅い方法は、deepsec の plugin 機構で Local LLM 用の AgentPlugin を追加することである。AgentPlugin は investigate() と revalidate() を実装すればよいので、内部で /v1/chat/completions 互換の Local LLM を呼び、deepsec が期待する JSON finding に変換すればよい。

LiteLLMを挟む場合

LiteLLM を使うと、この問題はかなり現実的に解決できる。LiteLLM Proxy は OpenAI-compatible な入口を提供し、背後に Ollama、vLLM、llama.cpp、各種 cloud provider などを接続できる。さらに Responses API 互換の入口として使える場合、deepsec の Codex backend から見て OpenAI 互換 endpoint として扱いやすくなる。

例えば、Ollama のローカルモデルを LiteLLM 経由で使うなら、概念的には次のような構成になる。

litellm --model ollama/qwen2.5-coder:32b --api_base http://localhost:11434

deepsec 側は LiteLLM Proxy を OpenAI endpoint として指定する。

OPENAI_API_KEY=anything
OPENAI_BASE_URL=http://127.0.0.1:4000
pnpm deepsec process --project-id <id> --agent codex --model qwen2.5-coder:32b

この構成では、deepsec から見た送信先は localhost の LiteLLM になる。さらに LiteLLM の upstream も Ollama や vLLM などのローカル実行にすれば、コード断片は外部に出ない。

ただし、注意点もある。

  • LiteLLM の upstream を cloud provider にすると、当然コードは外部に送られる
  • observability callback や logging を外部サービスに向けると、そこから漏れる可能性がある
  • Local LLM の精度次第で false negative が増える
  • triage は現状 Claude SDK 直呼びなので、完全ローカル化するなら別途対応が必要
  • Codex backend と LiteLLM の Responses API 互換性の細部で詰まる可能性はある

最初に試すなら、scan は通常通りローカルで実行し、process だけを LiteLLM 経由の --agent codex に向けるのがよい。

運用イメージ

閉域寄りの運用を考えるなら、次のような流れになる。

# 1. ローカルで候補抽出
pnpm deepsec scan --project-id my-app --root .

# 2. LiteLLM + Local LLM にだけ調査させる
OPENAI_API_KEY=anything \
OPENAI_BASE_URL=http://127.0.0.1:4000 \
pnpm deepsec process --project-id my-app --agent codex --model qwen2.5-coder:32b

# 3. 必要なら結果を export
pnpm deepsec export --project-id my-app --format md-dir --out ./findings

この場合、scan の段階では外部送信なし、process の段階でも LiteLLM と Local LLM が localhost に閉じていれば外部送信なし、という構成にできる。

まとめ

deepsec は、regex matcher による候補抽出と LLM による深い調査を分離している。この設計により、scan はローカルで安く広く実行でき、process は必要な箇所だけを高精度に調査できる。

外部送信を避けたい場合、問題になるのは scan ではなく process、revalidate、triage である。process と revalidate については、Codex backend の OPENAI_BASE_URL を LiteLLM に向ける構成が現実的な第一候補になる。LiteLLM の upstream もローカルに閉じれば、コードを外部 provider に送らずに運用できる可能性がある。

ただし、Local LLM の精度は商用の強い reasoning model と同等とは限らない。セキュリティレビュー用途では false negative が最も怖いので、閉域性と検出精度のトレードオフを理解したうえで使う必要がある。

tmuxのステータスバーにClaude Codeの動作状況を表示する

はじめに

Claude Code を日常的に使っていると、「今 AI が考えているのか、待っているのかわからない」という瞬間が出てくる。特に別のペインで作業しながら Claude Code を動かしているときに、ちらっとステータスバーを見るだけで状況が把握できると快適だ。

この記事では、Claude Code のフック機能と tmux の status-right を組み合わせて、AI エージェントの動作状況(idle / working / blocked)をリアルタイムで表示する仕組みを作る手順を整理する。

構成の概要

Claude Code フック
  → ~/.claude/agent-state (状態ファイル)
  → tmux refresh-client -S

tmux status-right
  → ~/.claude/tmux-agent-status.sh
  → 状態ファイルを読んでカラー表示

Claude Code には、ツール実行前・停止時・セッション開始・終了などに任意のシェルコマンドを実行できる「フック」機能がある。これを使って状態ファイルを更新し、tmux がその内容をポーリングして表示する。

状態は 3 種類:

状態意味表示
workingツール実行中・回答生成中シアン ⬤ claude
blocked権限確認待ちオレンジ ⊘ claude
idleセッション起動中・応答待ちグレー ○ claude

セッション終了時(release)は状態ファイルを削除し、tmux の表示からも消える。

セットアップ手順

1. フックスクリプトを作成

~/.claude/hooks/tmux-agent-state.sh を作成する。

#!/bin/sh
state="${1:-}"
state_file="${HOME}/.claude/agent-state"

case "$state" in
  working|blocked|idle) printf '%s\n' "$state" > "$state_file" ;;
  release) rm -f "$state_file" ;;
  *) exit 0 ;;
esac

tmux refresh-client -S 2>/dev/null || true
chmod +x ~/.claude/hooks/tmux-agent-state.sh

tmux refresh-client -S によって、フック実行直後にステータスバーが即時更新される。status-interval を待たずに反映されるのがポイント。

2. tmux 表示スクリプトを作成

~/.claude/tmux-agent-status.sh を作成する。

#!/bin/sh
state_file="${HOME}/.claude/agent-state"
[ -f "$state_file" ] || exit 0
state=$(cat "$state_file" 2>/dev/null) || exit 0
case "$state" in
  working) printf '#[fg=#8bd5ca,bold]⬤ claude#[nobold]' ;;
  blocked) printf '#[fg=#f5a97f,bold]⊘ claude#[nobold]' ;;
  idle)    printf '#[fg=#7d8996]○ claude' ;;
esac
chmod +x ~/.claude/tmux-agent-status.sh

カラーコードは Catppuccin Macchiato ベースのテーマに合わせている。自分の tmux テーマに合わせて #8bd5ca(シアン)と #f5a97f(オレンジ)と #7d8996(グレー)を調整すること。

3. Claude Code の hooks に登録

~/.claude/settings.json の hooks セクションに以下を追加する。各イベントに対応した state を渡す。

{
  "hooks": {
    "PermissionRequest": [
      {
        "hooks": [
          {
            "command": "bash '~/.claude/hooks/tmux-agent-state.sh' blocked",
            "timeout": 5,
            "type": "command"
          }
        ],
        "matcher": "*"
      }
    ],
    "PreToolUse": [
      {
        "hooks": [
          {
            "command": "bash '~/.claude/hooks/tmux-agent-state.sh' working",
            "timeout": 5,
            "type": "command"
          }
        ],
        "matcher": "*"
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "command": "bash '~/.claude/hooks/tmux-agent-state.sh' release",
            "timeout": 5,
            "type": "command"
          }
        ],
        "matcher": "*"
      }
    ],
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "bash '~/.claude/hooks/tmux-agent-state.sh' idle",
            "timeout": 5,
            "type": "command"
          }
        ],
        "matcher": "*"
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "command": "bash '~/.claude/hooks/tmux-agent-state.sh' idle",
            "timeout": 5,
            "type": "command"
          }
        ],
        "matcher": "*"
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "command": "bash '~/.claude/hooks/tmux-agent-state.sh' working",
            "timeout": 5,
            "type": "command"
          }
        ],
        "matcher": "*"
      }
    ]
  }
}

既存の hooks がある場合は、各イベントの hooks 配列に追記する形でマージする。

4. tmux.conf を更新

~/.tmux.conf の status-right の先頭に #(~/.claude/tmux-agent-status.sh) を追加する。

# 変更前
set -g status-right '#[fg=#9aa7b4,bg=#101419] CPU #{cpu_percentage} ...'

# 変更後
set -g status-right '#(~/.claude/tmux-agent-status.sh) #[fg=#9aa7b4,bg=#101419] CPU #{cpu_percentage} ...'

status-right-length が短いと表示が切れるため、必要に応じて広げる。

set -g status-right-length 120

設定を反映する。

tmux source-file ~/.tmux.conf

動作確認

スクリプト単体でテストできる。

# working 状態のテスト
echo "working" > ~/.claude/agent-state && ~/.claude/tmux-agent-status.sh

# idle 状態のテスト
echo "idle" > ~/.claude/agent-state && ~/.claude/tmux-agent-status.sh

# セッション終了(表示消去)のテスト
rm ~/.claude/agent-state && ~/.claude/tmux-agent-status.sh

tmux のステータスバーに ⬤ claude、○ claude、⊘ claude と表示されれば完成。

メリット

コンテキストスイッチの削減 Claude Code が作業中かどうかを確認するために Claude Code のペインにフォーカスを移す必要がなくなる。ステータスバーの隅を見るだけで把握できる。

blocked 状態の即時検知 ⊘ claude の表示で権限確認が必要なことがわかり、見逃しが減る。

シンプルな実装 状態ファイルを介した疎結合な設計なので、Claude Code のバージョン依存がなく壊れにくい。ファイルを読むだけなのでパフォーマンスへの影響もほぼない。

既存フックとの共存 herdr などの他のエージェント管理ツールのフックと並列で動かせる。settings.json の同じイベントに複数フックを追加するだけでよい。

まとめ

Claude Code のフックと tmux の #() コマンド展開を組み合わせることで、AI エージェントの状態をステータスバーに常時表示できる。実装はシェルスクリプト 2 本と JSON の設定変更のみで、既存の tmux 設定を大きく変える必要はない。

tmux refresh-client -S による即時更新により、status-interval の間隔(デフォルト 15 秒)を待たずにリアルタイムで状態が変わる点も実用的だ。

DoS攻撃に役立つdnsdist設定集

はじめに

この記事は、富士通クラウドテクノロジーズ Advent Calendar 2023の9日目の記事です。8日目では、o108minminさんのN100で楽しい自宅インフラでした。

DoS攻撃に役立つdnsdist設定集

最近、ランダムサブドメイン攻撃(DNS水責め攻撃)がかなり活発的に発生している。この記事では、dnsdistというツールを用いた攻撃対策について、ざっくり解説する。

※1 dnsdistについて、もっと詳しい情報は、DNS Summer Day 2019で発表されたdnsdist - DNSトラフィックに特化したオープンソースの多機能ロードバランシンサーをご覧ください。 ※2 ランダムサブドメイン攻撃について、もっと詳しい情報や対策全般については、DNS Summer Day 2023で発表されたランダムサブドメイン攻撃についてをご覧ください。 ※3 記事は、断片的な情報が記載されているので、公式ドキュメントを読むことを推奨する。

必要な基礎知識

dnsdistの設定はluaで書くことができるため、非常に柔軟な処理を実現することができるが、この記事では、複雑なlua設定を紹介せず、一旦組み込み関数の活用方法について、簡単にご紹介したい。組み込み関数とは、dnsdistに最初から予め用意された関数だ。これらの関数をluaで呼び出したり、組み合わせたりするイメージで設定を記述する。DoS攻撃に有効な対策として、個人的には以下の2つあると思う。

  • cache: cacheを使うことによって、データベース参照処理を大幅に削減することできるため、攻撃には全く同じクエリを効率的に処理することができる
  • block:
    • ランダムサブドメイン攻撃では、ランダムな値が使われるため、cacheの有効性が下がる場合がある。それを補うことができるのはblockだ。
    • dnsdistはロードバランサーの役割をしているため、自分が受付した全体パケットを集計し、特定のルール(Packet Policies)に該当するパケットに対し、指定された処理(Packet Action)を行う。
    • ルールは静的ルールと動的ルールがある。

Packet Action

ルールでマッチングされたパケットに対し、様々処理(Packet Action)を適用することができる。イメージしやすいようにいくつの例を記載しておく。

処理の関数名処理の概要
DelayActionレスポンスを指定したミリ秒だけ遅延させる
DropActionパケットをDROPする
RCodeAction指定されたrcodeでクエリをレスポンスに変えて即座に返信する

上はほんの一部に過ぎないので、詳細はドキュメントを参照すること。

静的ルール

MaxQPSIPRule

  • クライアントからの流量を一定固定する場合に有効である。

  • 例えば、下の例では、IPv4アドレスごと、およびIPv6の/48ごとのトラフィックを測定し、そのようなアドレス(範囲)のトラフィックが5qpsを超えた場合、100ms遅延させる。

    addAction(MaxQPSIPRule(5, 32, 48), DelayAction(100))
  • 静的ルールなので、クライアントの条件に柔軟な対応が難しい場合があるので、無難な値を設定する必要がある。

  • しかし、無難な値である反面、防御効果は下がっしてしまうことがある。

  • 後述のDynBlockRulesGroup:setQueryRate()等と組み合わせるとこの弱点を改善することができるであろう。

MaxQPSRule

  • 上で紹介したMaxQPSIPRuleの適用範囲は、クライアント毎であるのに対し、この MaxQPSRule の適用範囲は、全体である。全体の流量の上限を指定できる。
  • 例えば、以下の例では、全体の流量を5qpsで固定する。5qpsから溢れたパケットは全部DROPされる。
    • MaxQPSRule(5) : 5qpsでパケットをマッチングする
    • NotRule(MaxQPSRule(5) でNOTを取り、5qps以上のパケットをマッチングする
addAction(NotRule(MaxQPSRule(5)), DropAction())
  • この設定は適用範囲が広いため、防御効果は高いものの、スケールできないので、工夫する必要がありそう。

RegexRule

  • クエリを正規表現でマッチングする

  • 正規表現なので、様々なPacket Actionと組み合わせると、かなり柔軟なことができる

    • DropActionで特定のクエリのパケットを即座にDROPする
    addAction(RegexRule("[0-9]{4,}\\.example$"), DropAction())
    • RCodeAction で特定のクエリのパケットを即座にレスポンスする
    addAction(RegexRule("[0-9]{4,}\\.example$"), CodeAction(dnsdist.REFUSED))
  • blacklistのクエリを作って防御したり、データベース参照処理なしで即座レスポンスしたりすることができる

QPSAction

  • 流量制限を超えたパケットをDROPする

  • MaxQPSIPRuleとMaxQPSRuleよりもやることがシンプルなので、様々な組み合わせ方で防御効果を図れる

  • 例えば、RegexRuleと同じようにblacklistの作成を実現できる

    addAction("h4xorbooter.xyz.", QPSAction(10))
    addAction({"130.161.0.0/16", "145.14.0.0/16"} , QPSAction(20))
    addAction({"nl.", "be."}, QPSAction(1))

動的ルール(Dynamic Rule)

静的ルールによるPacket Actionと違い、動的ルールによるPacket Actionは生存時間が短く、ルールによって生成された後、またルールによって削除される。平常時静的ルールで守り、DoS攻撃のような一時的な異常時は、一時的な防御措置を適用することできる。

maintenance() 関数

maintenance()はdnsdistにより1秒おきに呼び出される関数である。この関数の中で動的ルールを定義することで、毎秒動的ルールでパケットがマッチングされる。マッチする場合は、そのルールの定義通りにdnsdistは処理する。

function maintenance()
  -- 動的ルール
end

DynBlockRulesGroup クラス

dnsdist 1.3以降では、動的ルールは基本的に DynBlockRulesGroup クラスを使ってまとめて定義することが推奨される。

-- dbrを作成する
local dbr = dynBlockRulesGroup()
-- このdbrに必要な動的ルールをどんどん追加していく
dbr:setQueryRate(30, 10, "Exceeded query rate", 60)
dbr:setRCodeRate(DNSRCode.NXDOMAIN, 20, 10, "Exceeded NXD rate", 60)
dbr:setRCodeRate(DNSRCode.SERVFAIL, 20, 10, "Exceeded ServFail rate", 60)
dbr:setQTypeRate(DNSQType.ANY, 5, 10, "Exceeded ANY rate", 60)
dbr:setResponseByteRate(10000, 10, "Exceeded resp BW rate", 60)

--- maintenance関数ではapply関数だけかけば良い
function maintenance()
  dbr:apply()
end

上の例で示されたように、 DynBlockRulesGroup クラスには set*() メソッドがたくさんある。ランダムサブドメイン攻撃に役に立つメソッドをいくつかピックアップし紹介する。

setRCodeRate()

このメソッドはレスポンスのコード(rcode)をベースにマッチングすることができる。ランダムサブドメイン攻撃では、ランダムな値が使われるため、殆どの場合では、NXDOMAINやREFUSEDのrcodeになる。これらのrcodeがある程度の流量で、引き起こているクライアントからパケットをマッチングすると、効果的に防御することができる。

dbr:setRCodeRate(DNSRCode.NXDOMAIN, 20, 10, "Exceeded NXD rate", 60)
dbr:setRCodeRate(DNSRCode.SERVFAIL, 20, 10, "Exceeded ServFail rate", 60)

setRCodeRatio()

  • ランダムサブドメイン攻撃の場合、各クライアントからは 1 qpsで、断続的攻撃されると、setRCodeRate() では、指定した流量が極端に小さい値になってしまう。
  • 値が小さい場合は、攻撃元だけではなく、一般ユーザも巻き込まれてしまう可能性がある。
  • この弱点を克服できるのは、setRCodeRatio() メソッド。
  • setRCodeRatio() メソッドは流量をqpsではなく、割合(ratio)でマッチングする。
  • 例えば、ランダムサブドメイン攻撃の場合、1 qpsで攻撃してくるクライアントでも、NXDOMAINのrcodeの割合が 80% 以上になる。一方で、一般クライアントは正常なクエリで問い合わせするため、この割合は50%より低い可能性が高いと考えられる。
  • このように割合で動的ルールを設定すると、攻撃はのクライアントと正常なクライアントの区別の精度がより高まる。
dbr:setRCodeRatio(DNSRCode.NXDOMAIN, 0.8, 10, "Exceeded NXD rate", 60)

まとめ

dnsdistを使ってランダムサブドメイン攻撃等DoS攻撃の防御方法について、浅く紹介した。静的ルールと動的ルールを使えば、ある程度攻撃の影響を限定することができる可能性があるが、もっと防御力を高めるためには、SaaS等を使用し、多重防止を構築することが重要だと思う。

最後に

10日目では、@ktakaakiさんのSwitchBotのBLEでデータを取ってみたです。お楽しみに!

herdrのsession・workspace・worktreeの使い分け

はじめに

herdr は、tmux のように端末内で動く永続セッション型のマルチプレクサだが、AI コーディングエージェント(Claude Code, Codex など)の状態を認識できる点が特徴のツールだ。tmux 用の設定を整理していて config.toml を触った際に、herdr 特有の session / workspace / worktree という3つの概念の関係が最初はわかりにくかったので、公式ドキュメントと herdr --help の実出力をもとに整理する。

全体像

herdr の管理単位は次のような階層になっている。

session (サーバー単位)
 └─ workspace (プロジェクト単位、tmux の session に近い)
     └─ tab (tmux の window に相当)
         └─ pane (実際のプロセスが動く場所)

worktree = git checkout の来歴を持った特殊な workspace

tmux に慣れていると「session」という言葉が一段ズレて感じられるが、herdr の session は tmux で言う「サーバープロセス」に近く、tmux の「session」に相当するのは herdr の workspace である。

session — サーバーインスタンスの単位

herdr はデフォルトで default という名前の session にアタッチ/起動する。--session オプションで名前を変えると、別の socket を持つ独立したサーバープロセスとして並行稼働できる。

herdr                # default session
herdr --session work # work という名前の別サーバー

用途(仕事用・個人用・検証用など)ごとにサーバーそのものを分けたいときに使う単位で、herdr session list --json で稼働中の session 一覧を確認できる。

$ herdr session list --json
{"sessions":[{"default":true,"name":"default","running":true,...}]}

workspace — プロジェクト単位

workspace は1つの cwd(基本的に1リポジトリ/1プロジェクト)に対応する管理単位で、中に複数の tab・pane を持てる。herdr のサイドバーに並ぶ行がこれにあたり、tmux で言うところの「session」に近い感覚で使う。

herdr workspace list
herdr workspace create --cwd ~/other-project --label other --no-focus

--no-focus を付けると、新しい workspace を作ってもフォーカスを奪わずバックグラウンドで起動できる。agent ごとの working / blocked / done といった状態はこの workspace 単位でサイドバーに表示される。

worktree — git ブランチを隔離した workspace

worktree は実体としては workspace の一種だが、git worktree の checkout と紐づいている点が異なる。worktree create は次の3つを1コマンドでまとめて行う。

  1. 指定したブランチを git worktree として checkout する
  2. その checkout を新しい workspace として開く
  3. 親リポジトリの workspace とグループ化する
herdr worktree create --branch tmux-cleanup --label tmux-cleanup

--branch に既存のローカルブランチ名を渡せばそれを checkout し、存在しなければ --base(省略時は HEAD)から新規作成する。checkout 先は --path を省略すると <worktrees.directory>/<repo>/<branch-slug> になる。

これにより、同じリポジトリの複数ブランチをそれぞれ別 workspace として並行して agent に触らせる、といった使い方ができる。main ブランチ側の作業とブランチ側の作業が、workspace = pane 群単位で完全に分離される。

削除は worktree remove で行う。

herdr worktree remove --workspace <id>

これは git worktree remove 相当の処理で、herdr 側の workspace 状態と checkout の実ファイルは消えるが、ブランチ自体は残る。dirty な checkout の場合のみ --force が必要になる。なお workspace close は herdr 側の状態を閉じるだけで、checkout のファイルは残る点が worktree remove との違いになる。

使い分けの目安

したいこと使う概念
仕事用/個人用で完全に別サーバーにしたいsession
同じマシンで別プロジェクトを並行して触りたいworkspace
同じリポジトリで複数ブランチを agent に並行で触らせたいworktree
同じ workspace 内でログ用・エディタ用など画面を分けたいtab / pane

まとめ

herdr の session はサーバープロセス、workspace はプロジェクト単位(tmux の session に相当)、worktree は git checkout の来歴を持った特殊な workspace、という3層構造で理解すると迷わない。特に worktree は単なる git worktree のラッパーではなく、「checkout → workspace 化 → 親リポジトリとのグループ化」までを1コマンドで行ってくれる点が、ブランチ単位で agent を並行稼働させたいときに便利だ。

Headroom × Context Mode:コンテキスト効率化ツールの比較と実装

はじめに

LLM エージェント(Claude Code, OpenCode など)を使い込むと、すぐにコンテキスト問題に直面する。ファイル読み込み、ツール出力、ログ検索、git diff — 全部が context window を圧迫し、30 分も作業すると 4 割前後がツール出力で埋まる。

この問題に対して、レイヤーの異なる 2つのアプローチがある:

  1. Headroom — LLM に送る情報(会話履歴・tool 出力・RAG)を送信直前に圧縮(API トランスポート層)
  2. Context Mode — ツール出力をローカル処理して、そもそもトランスクリプトに入れない(エージェント/hook 層)

この記事では、2つのツールの設計思想の違い、実装レベルでの違い、そして組み合わせるときの実践的な手順を整理する。

まず:Claude Code のようなエージェントはどう動くのか

後で出てくる図を読むには、Claude Code のような「LLM エージェント」がどう動くかを先に押さえる必要がある。ここだけは前提知識として、できるだけかみ砕いて説明する。

ポイントは1つ。LLM(AI の本体)は、前の会話を覚えていない。だから毎回、「これまでの会話ぜんぶ」をもう一度まとめて渡してあげないと話が通じない。

たとえるなら、LLM は記憶力がゼロの天才だ。とても賢いが、1回答えるたびに記憶がリセットされる。だから話しかけるたびに、これまでのやりとりを書いたノートを丸ごと最初から読ませるしかない。エージェント(Claude Code)は、その「ノートを毎回渡す」係だと思えばいい。

動きを番号付きで追うとこうなる。

① あなたが指示する(例:「このログのエラー原因を調べて」)
      ↓
② これまでの会話を“ぜんぶ”まとめて LLM に送る      ← 「送信」の瞬間
      ↓
③ LLM が答えを返す or「道具を使いたい」と言う
      ↓
④ 道具(ツール)を実行する
   (ファイルを読む / コマンドを走らせる / 検索する)
      ↓
⑤ 道具の出力が“ノート(会話)”に追記される        ← ここがどんどん膨らむ
      ↓
   (②に戻ってくり返す)

ここで2つの言葉を覚えておく。

  • コンテキスト(ノート):②で毎回送る「これまでの会話ぜんぶ」。LLM の作業机のようなもの。
  • コンテキストウィンドウ:その机の広さ(=1回に送れる上限)。無限ではない。

問題は ⑤ だ。kubectl logs やコード検索のように、道具の出力が巨大だと、それがそのままノートに貼り付けられて机を埋めていく。机が埋まれば、本来の作業に使えるスペースが減る。「30 分で 4 割がツール出力」というのは、この⑤がたまり続けた結果だ。

ここまで分かると、この記事の2ツールはこの図のどこに割り込むかの違いだと一言で言える。

  • Context Mode … ④〜⑤に割り込む。道具の出力が巨大なら、そもそもノートに貼らずに要約だけ残す。
  • Headroom … ②の直前に割り込む。送る瞬間に、ノート全体をぎゅっと圧縮してから渡す。

以降の各ツールの「タイミング」は、この①〜⑤の番号を指していると思って読んでほしい。

1. ツール概要と比較テーブル

Headroom(chopratejas/headroom)

何か:LLM エージェント向けのコンテキスト圧縮エンジン

動作原理:

messages → ContentRouter → 
  ├─ JSON → SmartCrusher (50-90% reduction)
  ├─ Code → CodeCompressor (AST解析, 40-70%)
  └─ Text → Kompress-base (ML model, 30-80%)
     ↓
  CacheAligner (prefix stabilization)
     ↓
  CCR reversible layer (originals in SQLite)
     ↓
  LLM

主な特徴:

  • タイミング:②の直前(LLM送信直前 / post-execution)
  • 圧縮単位:メッセージシーケンス全体
  • デプロイ方法:Proxy, Library, MCP, CLI wrap
  • 復元性:CCR で完全復元可能(SQLite TTL 付き)
  • 可用性:言語・プラットフォーム非依存

CCR / SQLite TTL とは:Headroom の圧縮は原文を「捨てる」のではなく「畳んで隠す」方式だ。削った原文はローカルの SQLite に退避し、本文側にはプレースホルダだけを残す。この原文を退避して後から巻き戻せる可逆レイヤーが CCR(図の "CCR reversible layer")で、必要になればプレースホルダから原文をそのまま復元できる(lossless)。退避した原文には **TTL(Time To Live=保持期限)**が付いていて、一定時間で自動的に消える。これにより「あとから完全復元できる」一方で、SQLite が無限に肥大化しないようにしている。

(注:本記事では "CCR" を可逆レイヤーの呼称として扱う。正式な略称の展開は未確認なので、公開前に Headroom の README で確認のこと。)

実績:

場面圧縮率
Code search (100結果)92%
SRE incident debugging92%
GitHub issue triage73%
Codebase exploration47%

Context Mode(mksglu/context-mode)

何か:Claude Code プラグイン + MCP サーバ。ツール出力の自動ルーティングと session tracking

動作原理:

tool call
  ↓
[PreToolUse hook] 
  ↓
raw output ← tool execution
  ↓
[PostToolUse hook] 判定:
├─ small → context に
├─ large → ctx_execute で処理
│          ↓
│        script 実行
│          ↓
│        ctx_index (FTS5)
│          ↓
│        summary だけ context に
│
└─ compacting → FTS5 search で復元

主な特徴:

  • タイミング:④〜⑤(ツール呼び出しの前後。hook で routing を強制 + イベント記録)
  • 処理単位:ツール単位のローカルスクリプト実行("think in code")
  • デプロイ方法:Claude Code plugin / OpenCode plugin など hook 対応エージェント + MCP
  • 復元性:FTS5 BM25 検索で復元(キーワードベース)
  • 可用性:16 プラットフォーム対応(Claude Code, OpenCode, Gemini CLI, Cursor など)

注意:Context Mode は Headroom のように「出力されたバイト列を透過的に圧縮する」のではない。hook が raw な Bash/Read/WebFetch を抑制してモデルに ctx_execute(スクリプト実行)を 書かせる よう仕向け、結果のサマリーだけを context に入れる。つまり「モデルの振る舞いを変える」アプローチである。

実績(README より):

場面削減
Playwright snapshot56 KB を context に入れない
GitHub Issues 20件59 KB → 集計サマリーのみ
session 全体315 KB → 5.4 KB(98%)
"think in code" 例47×Read (700 KB) → 1×ctx_execute (3.6 KB)
OpenCode セッション~98% saved(README 計測値)

比較テーブル

項目HeadroomContext Mode
圧縮対象既にcontextにある情報全体tool出力(contextに入る前)
圧縮アルゴリズムML model (Kompress-v2) + AST決定的なスクリプト実行
復元方法CCR tool で完全復元FTS5 search でキーワード検索
依存性MCP 不要(Proxy/Library で可)hooks が必要(自動化のため)
使用対象全LLM API (Anthropic/OpenAI/Bedrock)hook 対応エージェント 16種 (Claude Code / OpenCode 等)
output削減Verbosity steering + Effort routing— (prose-style は強制しない方針)
Session memoryCross-agent memorySQLite + hooks tracking
設定難易度低(headroom wrap 一発 or Proxy起動)低(plugin install)
圧縮率47-92% (場面依存)~98% (raw tool出力が大きい場合)

圧縮率は測定対象が異なるため直接比較できない。Headroom は「送信リクエスト全体のトークン削減率」、Context Mode は「raw tool 出力を context に入れないことによる削減率」を指す。レイヤーが違うので、後述の通り組み合わせると相補的に効く。


どちらをいつ使うか

Headroom が向いている場面:

  • 複数ツール間で context を共有する必要がある
  • API response が JSON で深い(SmartCrusher 向き)
  • Code review や long diff を読ませたい
  • 複数のLLM provider を使い分けたい

Context Mode が向いている場面:

  • rg --json, kubectl logs, git log など raw 出力が巨大
  • 同じ調査・デバッグを何度も繰り返したくない
  • session 中に「最後の編集ファイル」を自動検索したい
  • Claude Code での日常作業

両方組み合わせるべき場面:

  • 長期 debugging session(Context Mode で即座に削減 + session 記憶)
  • Conversation compacting 後の resume(Context Mode で FTS5 検索 + Headroom で再圧縮)
  • multi-agent 運用(Headroom で cross-agent memory + Context Mode で Claude Code local routing)

2. 実践的な実装手順

2.1 Claude Code での Context Mode セットアップ

前提条件:Claude Code v1.0.33+

手順:

# 1. Plugin install
/plugin marketplace add mksglu/context-mode
/plugin install context-mode@context-mode

# 2. Reload
/reload-plugins

# 3. Verify
/context-mode:ctx-doctor
# ✅ RuntimeCheck, HooksCheck, FTS5Check all pass

自動的に有効になること(plugin が 4 hook を登録):

  • SessionStart hook:routing 指示を runtime で注入(プロジェクトにファイルは書かない)
  • PreToolUse hook:raw な tool 呼び出しを routing(ctx_* へ誘導)
  • PostToolUse hook:実行結果(編集・git・error・決定)を session DB に記録
  • PreCompact hook:Compacting 直前に FTS5 へ index 化して resume 用 snapshot を準備
  • 11 個の MCP tools:6 sandbox(ctx_execute, ctx_search, ctx_index など)+ 5 meta(ctx_stats, ctx_doctor など)

設定ファイル(optional):

{
  "statusLine": {
    "type": "command",
    "command": "context-mode statusline"
  }
}

2.2 OpenCode での Context Mode セットアップ

OpenCode は TypeScript プラグイン方式に対応している。Claude Code のような /plugin マーケットプレイスではなく、opencode.json に plugin を登録する。

前提条件:Node.js >= 22.5(または Bun), OpenCode installed

手順:

# 1. opencode.json に追記
#    (プロジェクト直下、またはグローバルは ~/.config/opencode/opencode.json)
{
  "$schema": "https://opencode.ai/config.json",
  "plugin": ["context-mode"]
}

この plugin エントリだけで、11 個の ctx_* tool が in-process で登録され、hook が有効になる(OpenCode が context-mode の TS プラグインを直接呼ぶので、stdio MCP の子プロセスは立たない)。

# 2. (任意) routing rules ファイルをコピー
#    モデルがどの tool を使うべきか・どのコマンドがブロックされるかを認識する
cp node_modules/context-mode/configs/opencode/AGENTS.md AGENTS.md

# 3. OpenCode を再起動

# 4. Verify — セッション内で `ctx stats` と入力

注意点:

  • OpenCode には本物の SessionStart hook が無い(sst/opencode#14808)。プラグインは experimental.chat.system.transform を代替として使い、routing block と前回セッションの snapshot を system prompt に注入する。
  • routing 強制は tool.execute.before / tool.execute.after で行われる。
  • 既存 config に plugin: ["context-mode"] と mcp.context-mode の両方があると ctx_* tool が 0 個になる。context-mode upgrade で legacy MCP エントリを削除する。

2.3 VS Code(GitHub Copilot)での Context Mode + Headroom

GitHub Copilot を VS Code で使っている場合の手順。Copilot は /plugin マーケットプレイスを持たないので、Context Mode は MCP サーバ + hook ファイルを手動で置く方式になる。Headroom は別レイヤー(proxy)で被せる。

Context Mode(VS Code Copilot)

前提条件:Node.js >= 22.5(または Bun), VS Code + Copilot Chat v0.32+(hook 対応バージョン)

手順:

# 1. context-mode を global install
npm install -g context-mode
// 2. プロジェクト直下に .vscode/mcp.json を作成
//    (ctx_* tool を Copilot Chat に登録する)
{
  "servers": {
    "context-mode": {
      "command": "context-mode"
    }
  }
}
// 3. .github/hooks/context-mode.json を作成
//    (routing 強制 + session 記録。PreCompact も足せる)
{
  "hooks": {
    "PreToolUse": [
      { "type": "command", "command": "context-mode hook vscode-copilot pretooluse" }
    ],
    "PostToolUse": [
      { "type": "command", "command": "context-mode hook vscode-copilot posttooluse" }
    ],
    "SessionStart": [
      { "type": "command", "command": "context-mode hook vscode-copilot sessionstart" }
    ]
  }
}
# 4. VS Code を再起動

# 5. Verify — Copilot Chat で `ctx stats` と入力
#    ctx_* tool が出てきて応答すれば OK

自動的に有効になること:

  • SessionStart hook:routing 指示を注入(②の前提知識を毎セッション渡す)
  • PreToolUse hook:raw な tool 呼び出しを ctx_* へ誘導(④〜⑤に割り込む)
  • PostToolUse hook:編集・git・error・決定を session DB に記録
  • PreCompact hook:上の 3 つに加えて足せば、compaction 直前に FTS5 へ index 化(configs/vscode-copilot/hooks.json に全部入りの例がある)

routing rules(任意):モデルにどの tool を使うべきか・どのコマンドがブロックされるかを明示したい場合は、ルールを .github/copilot-instructions.md にコピーする。

cp node_modules/context-mode/configs/vscode-copilot/copilot-instructions.md .github/copilot-instructions.md

注意:Claude Code 版(2.1)が plugin 1 発で 4 hook を登録するのに対し、VS Code Copilot は .vscode/mcp.json(MCP)と .github/hooks/context-mode.json(hook)を別々に置く必要がある。どちらか片方だけだと「tool は見えるが routing が効かない」「routing は効くが ctx_* が無い」状態になるので両方必須。

Headroom(VS Code Copilot)

VS Code Copilot で Headroom を使う経路は 2 種類ある。透過圧縮の有無が大きく異なる。

方法1:MCP(推奨スタート)

.vscode/mcp.json に headroom MCP サーバを追加すると、Copilot Chat が headroom_compress / headroom_retrieve / headroom_stats を tool として呼べるようになる。Context Mode と並べて書ける。

// .vscode/mcp.json(context-mode に headroom を追記)
{
  "servers": {
    "context-mode": {
      "command": "context-mode"
    },
    "headroom": {
      "command": "headroom",
      "args": ["mcp", "serve"]
    }
  }
}
# headroom のインストール(未済の場合)
uv tool install "headroom-ai[all]"

# VS Code を再起動してツールが認識されたか確認
# Copilot Chat で「headroom stats を見せて」などと聞くと
# headroom_stats tool が呼ばれる

この方法ではモデルが明示的に headroom tool を呼んだときだけ圧縮が走る。自動透過圧縮は効かないが、すぐ試せる。

方法2:Proxy(BYOK / カスタムモデル使用時)

VS Code 拡張の Copilot は通常 GitHub ホストのモデルに繋ぐため、ANTHROPIC_BASE_URL で接続先を差し替えられない。proxy を通せるのは Copilot のカスタムモデル(BYOK / OpenAI 互換エンドポイント) を設定している場合のみ。

# Proxy を起動
headroom proxy --port 8787

その上で、VS Code の Copilot カスタムモデル設定の base URL を http://127.0.0.1:8787 に向ける。これで全 API リクエストが透過圧縮される。

この経路は本記事の検証対象 — 「どの VS Code 設定キーで base URL を差し替えるか」「対応 Copilot バージョン」は検証後に追記する。標準の GitHub モデルのみ使っている場合はこの経路は使えない。

まとめ:VS Code Copilot では Context Mode(hook + MCP 手動配置)は確実に動く / Headroom は MCP 方式ならすぐ動く、透過圧縮(proxy)は BYOK 時のみ。まず Context Mode + Headroom MCP を入れ、ctx stats と headroom_stats の両方で削減量を確認するのが手堅い出発点。

2.4 Headroom の設定

Headroom は API のリクエストを透過的に圧縮するプロキシとして動く。ルーティングは HTTP プロキシ(http_proxy)ではなく、ANTHROPIC_BASE_URL / OPENAI_BASE_URL をローカルプロキシに向ける方式である点に注意。

前提条件:uv。以下のいずれの方法でも、まず Headroom 本体を入れておく。

uv tool install "headroom-ai[all]"
# → headroom コマンドが PATH に入る
#   インストール確認: headroom --version

方法D(Library)でプロジェクトに直接組み込む場合は、ツールではなく依存として uv add "headroom-ai[all]" を使う。

以降の方法A〜Dは、この headroom が入っている前提で進める。

方法A:Claude Code を wrap(最も簡単)

# wrap で起動(proxy 起動 + ANTHROPIC_BASE_URL 設定を自動でやる)
headroom wrap claude
# → 内部で `ANTHROPIC_BASE_URL=http://127.0.0.1:8787` 相当を設定し Claude Code を起動

headroom wrap の対応エージェント:claude, codex, cursor, aider, copilot, gemini。

方法B:Proxy を手動起動して任意のエージェントを向ける

# 1. Proxy 起動
headroom proxy --port 8787

# 2. エージェント側で provider の base URL をプロキシに向ける
#    Claude Code を手動で向ける場合:
export ANTHROPIC_BASE_URL=http://127.0.0.1:8787

OpenCode は headroom wrap の対応リストに無い。OpenCode は OpenAI 互換クライアントなので、headroom proxy を立てて OpenCode の provider 設定(opencode.json の provider baseURL)をプロキシに向ける方式になる。この経路は本記事の検証対象(シナリオ参照)— 正確な設定は検証後に追記する。

方法C:MCP(Claude Code 向け)

headroom mcp install
# → Claude Code 再起動で headroom_compress / headroom_retrieve / headroom_stats が使える

方法D:Library(Python/TypeScript apps に直接組み込み)

from headroom import compress

messages = [...]
compressed = compress(messages, model="claude-sonnet-4-6")
# → 圧縮後のメッセージを Anthropic SDK に送信

2.5 組み合わせ運用(Context Mode + Headroom)

シナリオ:大規模コードベース調査

┌─ Codebase scanning task
│
├─ [Claude Code / OpenCode]
│  └─ User: "Find all uses of API X in the codebase"
│
├─ [Context Mode PreToolUse hook]
│  └─ Detects: rg --json output (5MB)
│  └─ Routes: ctx_execute で filter script に
│     ↓
│     (JavaScript)
│     const results = JSON.parse(input)
│       .filter(r => r.path.includes('src'))
│       .slice(0, 50)  // Top 50 only
│     console.log(`${results.length} matches found`)
│     results.forEach(r => console.log(`${r.path}:${r.line}`))
│     ↓
│     Output: 2.4 KB
│
├─ [ctx_index hook]
│  └─ FTS5に索引化(full results も保存)
│
├─ [次ターン送信前: Headroom proxy が request を圧縮]
│  └─ 圧縮対象は「送信リクエスト=これまでの会話履歴+残った tool 出力」
│  └─ SmartCrusher: JSON を 50-90% / CodeCompressor: AST を 40-70%
│  └─ CacheAligner: provider KV cache のヒット率向上
│
└─ [Output token reduction (optional, HEADROOM_OUTPUT_SHAPER=1)]
   └─ Verbosity steering / Effort routing でモデルの「書き戻し」を削減

結果(2つは別レイヤーなので相補的):
  - context に入る raw 量: Context Mode が削減(2.4 KB に)
  - 送信リクエストのトークン: Headroom がさらに圧縮
  - 出力トークン: Headroom output shaper が削減
  - Session DB: full results は FTS5 に indexed(ctx_search で後から復元)

ポイント:Context Mode はトランスクリプトに入る情報を減らし、Headroom は API へ送る/返るバイト列を圧縮する。動作レイヤーが違うため二重圧縮にならず、片方が削り残した分をもう片方が拾う。ただし両者とも "think in code" 的な思想を持ち、Headroom は内部で RTK(shell 出力書き換え)を同梱するため、シェル出力まわりは役割が一部重なる。

実装手順:

  1. Context Mode 起動(Claude Code plugin)

    /context-mode:ctx-stats  # 確認
  2. Task 実行

    User: "Find all imports of ./utils in this repo"
    
    Claude Code (自動的に):
    → $ rg --json "import.*utils" src/
    
    Context Mode (自動):
    → PreToolUse: 出力サイズ > threshold → ctx_execute へ
    → ctx_execute で集計スクリプト実行
    → 結果 (2.3 KB) + indexed summary
  3. Compaction 後の resume

    Context Mode (自動):
    → PreCompact hook: FTS5 index を最新化
    
    User: "中断した調査を続けて"
    
    Claude Code:
    → ctx_search("utils imports") で過去の結果を検索
    → Session DB から前回の編集ファイル等を復元
  4. Headroom での最終圧縮(optional)

    # Context Mode plugin を有効にしたまま、Claude Code を Headroom 経由で起動
    headroom wrap claude
    # → ANTHROPIC_BASE_URL がプロキシに向き、送信リクエストが圧縮される
    #   Context Mode の hook/MCP はそのまま動く(レイヤーが違うため共存可能)

    共存時の注意:両方とも RTK/shell 出力の取り扱いに触れるため、headroom wrap 側の HEADROOM_CONTEXT_TOOL と Context Mode の routing が二重に効いていないか、/context-mode:ctx-stats と headroom perf の両方で実測して確認すること。


まとめ(現時点)

ここまでを整理すると、2つのツールは競合ではなく別レイヤーで効く補完関係だ。

  • Context Mode は「そもそも context に入れない」。tool 出力が巨大なとき(rg --json, kubectl logs, CI ログなど)に直撃で効く。plugin を入れるだけで動き、Claude Code の日常作業で一番手軽。
  • Headroom は「送信直前に圧縮する」。会話履歴も残った tool 出力もまとめて削るので、Context Mode が削り残した分を拾える。headroom wrap claude で両立する。
  • 迷ったら まず Context Mode、効果が頭打ちになったら Headroom を被せる、という順番が分かりやすい。

一方で、本文中の数値(圧縮率・対応プラットフォーム数・ツール数・バージョン要件など)は、どちらのツールも更新が速く変わりやすい。公開前に各 README で最新の値を確認してほしい。