Cloudflare D1を読み書きできる、JetBrains IDE向けのJDBCドライバーをKotlinで作ってみた

Cloudflare D1を使ったアプリを作っていると、D1の中身を確認したり更新したりしたくなることがあります。

その際、使い慣れているJetBrains IDEやDataGrip(以降は DataGrip )で行えるとよいです。

 
ただ、執筆時点では、Cloudflare公式のD1向けJDBCドライバーを見つけられませんでした。

また、コミュニティ系のJDBCドライバーは存在しますが、DataGripでの表形式によるデータ編集には対応してなさそうでした。

 
そこで、Claude Codeとともに、Cloudflare D1向けに、JetBrains IDE専用のJDBCドライバーをKotlinで書いてみたことから、メモを残します。

 
目次

 

環境

  • mac
  • Claude Desktop 1.30096.1
  • OpenSpec 1.8.0
  • JDK 21
  • Kotlin 2.4.10
  • Kotest 6.2.3
  • Gradle 9.7.0

 
なお、今回はClaude DesktopのSSHセッションを使ってdevcontainerへと接続し、devcontainerの中でClaude Codeを実行しています。

 

技術スタックについて

実装はClaude Codeに任せることもあり、可能な限り依存の少ないパッケージ構成を選びました。

  • プロダクションコード
    • Kotlinの標準ライブラリ ( kotlin-stdlib ) のみ
      • Cloudflare D1と通信するときに必要なJSONパーサーなどは自作
  • テストコード
    • Kotlinの標準ライブラリに加え、Kotestも利用
      • Kotestではプロパティベーステストも扱えることから、自分でプロパティベーステスト基盤を作るよりは手間がかからなさそうと判断

 
また、同じJVMに別バージョンのkotlin-stdlibが同居することによる衝突を避けるため、 kotlin-stdlibのrelocationを行っています。

 

Cloudflare D1向けJDBCドライバーの注意点

以下のことに気をつけて作りました。

 

アクセス時のURLに秘匿情報を乗せない

Cloudflare D1へはAPIを経由してアクセスします。その時、 User API TokenとAccount API Tokenのどちらかが必要です。

Account API Token は /user/* 系の API を呼べないことから、今回のドライバーは D1 の /accounts/ 配下の API だけを使うことにしました。この結果、User API Token と Account API Token のどちらでも動くようになりました。

例えば、Account API Tokenは、以下のドキュメントに従って発行できます。
Account API tokens · Cloudflare Fundamentals docs

 
APIトークンは秘匿情報にあたることから、JetBrains IDEの画面に平文で閲覧できないようにします。

そこで、アクセス時のURLにAccount IDやAPIトークンを設定する代わりに、以下の値として渡すようにしました。

  • Account ID
    • userに設定する
  • APIトークン
    • passwordに設定する

 
これにより、URLは jdbc:cloudflare:d1://<database_id> となり、データベースIDだけが乗ることになります。

 

対話的トランザクションが無くても、DataGripでの編集を可能にする

Cloudflare D1では、 BEGINCOMMIT を複数リクエストにまたがって行う、対話的トランザクションがサポートされていないようです。

 
一方、今回DataGripの挙動を確認してみたところ、DataGripで編集するときは Tx: Auto であっても、

  • setAutoCommit(false) で、トランザクションを開始
    • 合わせて setTransactionIsolation() も呼ばれる
  • rollback() で、セッションをきれいにするための防御的なrollbackを実施
  • DML を実行
  • commit() でDMLの結果を確定

という順で動くように見えました。

 
そのため、トランザクションがないからと言って、

  • setAutoCommit(false)
  • setTransactionIsolation()
  • rollback()
  • commit()

を実装せずに「未対応エラー」としてしまうと、DataGripでの編集でのSubmitが失敗してしまいます。

 
今回のドライバーは JetBrains IDE専用のため、JDBC の一般的な作法に従うよりは、JetBrains IDEで動くことを優先しました。

そこで、 writesInManualMode というフラグで状態を管理して画面操作に対応できるようにしています。

タイミング writesInManualModeの値
setAutoCommit(...) が呼ばれた false にリセット
自動コミット OFF 中に「INSERTなど、書き込みの可能性がある文」を実行した trueにする
commit() が呼ばれた false にリセット(確定したので取り消し対象は消えた)

 
そのうえで、 commit()rollback() を次のように扱います。

  • commit()
    • 何もせずに成功を返す
      • D1では各文は送信した時点で確定済みなので、「成功」という戻り値で問題なく、何もしなくて良いため
  • rollback()
    • writesInManualModefalse なら、取り消すべき書き込みが無いので何もせず成功とする
    • true なら、書き込みの可能性がある文がすでに D1 で確定していて取り消せないため、SQLFeatureNotSupportedException を投げる
      • エラーメッセージとして「実行済みの文の効果は取り消されず、残っています」を渡す

 
また、DataGripの挙動として、以下もあったのでメモしておきます。

  • Tx: Auto であっても、1セルの変更であっても、無条件にトランザクション操作となる
  • supportsTransactions()false と設定していても参照されない
  • Test Connection では、 Connection.getWarnings() を呼び、 0A000 が返るとそこで失敗扱いになる

 

DBMS名をSQLiteにする

DataGripでデータを編集する場合、DMLの生成はDialect設定に従うのではなく、JDBCの getDatabaseProductName() で決まるようでした。

これにより、Dialect設定を SQLite にしていたとしても、DBMS名( getDatabaseProductName() )を返信しない( getMetaData が非対応エラーを返す)状態だと、SQLiteでは非サポートの as のないテーブル別名が付いた状態でUPDATEなどが発行されてしまいます。

そのSQLをD1へ送ったとしても、D1からはエラーが返ってきてしまいます。

 
そこで、ワークアラウンド的に以下の対応をしています。なお、D1の実体はSQLiteなので問題なく動くようです。

  • DBMS名は SQLite
  • バージョンは 3.45.1
    • 3.45の jsonb はD1がサポートしていたものの、 3.48 の if() はサポートしていなかったことから、 3.45.1 を採用した
    • なお、 sqlite_version()PRAGMA compile_options は D1 が拒否したため、それらは使えなかった
  • 接続先はD1
    • getDriverName()Cloudflare D1 JDBC Driver for JetBrains IDEs と返すことにより、識別できるようにした

 

スキーマは main にしないと、DataGrip上にツリーが出ない

getCatalogs()getSchemas() が両方空だと、DataGripがテーブルの置き場所(ノード)を作ることができず、 getTables() が呼ばれませんでした。

この結果、ツリーが空のままで表示されてしまいました。

そこで、スキーマ名として main を返すことで、この事態を回避しました。

 

動作確認

環境設定

ドライバーの設定

DataGripのDatabaseツールウィンドウにある + をクリックし、 Driver を選択します。

次に、Driver Filesの + をクリックし、 ./build/libs/d1-kotlin-jdbc-driver-0.0.3-all.jar を選択します。

その後、Classで dev.thinkami.d1jdbc.D1Driver を選択します。

 

データソースの設定

Cloudflareのダッシュボードを参照しながら、以下を設定します。

  • User
    • Account ID
  • Password
    • Account API Token
  • URL
    • jdbc:cloudflare:d1://<database_id>

 
あとは Test Connection ボタンで接続できることを確認し、その後データが表示されればOKです。

 

動作確認時のスクリーンショット

今回はidが 750 のBreezeリンゴの部分を編集します。

編集する前は、 [リンゴ]今日は... の間に半角スペースが入っています。

 
続いて、半角スペースを削除します。これにより、 Submit ボタンが有効になっています。

 
Submitすると、更新が完了しました。リロードしても更新後の値のままになっていることから、成功しているようです。

 

謝辞

リトライ条件・D1 非対応 PRAGMA への静的応答・内部テーブルの除外・meta フィールドと JDBC API の対応付けといった「挙動の仕様」は、前述のコミュニティ版JDBCドライバーのJava実装から多くを学びました。ありがとうございました。

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi/d1-kotlin-jdbc-driver

#セキュリティのアレ でRSS配信されなくなった過去回を、自分用に聴けるようにするRSSフィード生成ツールをPythonで作ってみた

最近、散歩をするときにポッドキャスト「セキュリティのアレ」を聴いています。
podcast - #セキュリティのアレ - ゆるーいセキュリティのポッドキャストですよ。

最近のセキュリティ情報がゆるく紹介されることもあり、毎回楽しく聴いています。

 
そんな中、最新のエピソードだけではなく、過去の第1回から遡って聴いてみようと考えました。

しかし、自作のポッドキャストプレイヤーでは、第12回までしか取得できていないようでした。

 
自作のポッドキャストプレイヤーのバグかなと思い、公式のRSSフィードを確認したところ、一番古いのが第15回になっていました。
https://www.tsujileaks.com/?feed=podcast

これより、次のことが分かりました。

  • 自作のポッドキャストプレイヤーで購読開始したときは取得できていた回(12,13,14回)が、今は取得できなくなっていること
  • どうも、最新の300件までしか公式ではRSSフィードを配信していないようであること

 
過去の配信回が気になったことから、次は「セキュリティのアレまとめ」サイトを見に行きました。

そこの放送回一覧を見たところ、以下と分かりました。

これより、何らかの理由でRSSフィードがないものの、エピソードとしては公開されていることが分かりました。

 
次に、エピソードの音源がどこにあるかについて調べたところ、セキュリティのアレまとめ様が公開してくださっているリポジトリのCSVファイルに、RSSフィードの音源URL ( audio_url 列)が含まれていました。
Security-no-ARE-words/source/static/セキュリティのアレ放送回リスト_自動更新.csv at main · BerandaMegane/Security-no-ARE-words

これより、「このCSVファイルを元に自分でRSSフィード(XML)を作り出し、それを何かしらの形でホストして、その後ポッドキャストプレイヤーに読み込ませれば、聴くことができるようになるのでは」と考えました。

そこで、Python製のツールとして作ってみたことから、メモを残します。

 
目次

 

環境

  • mac
  • Python 3.14.7
    • Pythonを採用した理由は以下です
      • 自分が慣れていること
      • 標準ライブラリだけで実装からテストまでできること
      • セキュリティのアレまとめ様でPythonを使っていたこと
  • Claude Desktop 1.30096.1
  • OpenSpec 1.8.0

 

設計段階で検討したこと

guidをどうするか

RSS2.0では、 item の子要素として guid があります。この guid の値を使うことで、RSSフィード内での同一性を表すことができます。
https://www.rssboard.org/rss-specification#ltguidgtSubelementOfLtitemgt

自作のポッドキャストプレイヤーでも、 guid を元に「すでにこのエピソードは取り込んだかどうか」を判定しています。

ここで、セキュリティのアレまとめ様のCSVを確認すると、 guid 列は見当たりませんでした。

 
続いて、公式のRSSフィードの guid を見たところ、

  • 古いものは http://www.tsujileaks.com/?p=***
  • 途中で、プロトコル部分が httphttps が混在
  • 2023年8月頃を最後に http が現れなくなり、以降は https で固定

という状況でした。

 
今回作成しようとしている範囲は、いずれも http 時代のものに見えたことから、 guid はプロトコル部分を http にしたものを利用することにしました。

また、今後も新しい回が公開されるたびに古いものが消えていくことが予想されたため、今のうちにRSSフィードに含まれる過去の guid のリストを保存しておくことにしました。

 

XMLファイルのホストはどうするか

ポッドキャストプレイヤーで取り込むには、XMLをRSSフィードとして配信する必要があります。

手っ取り早く、かつ、使い終わったら削除しやすいのがGitHubのSecret Gistだったため、今回はGistに生成したXMLファイルを貼り付け、それを読み込ませることにしました。

 
ちなみに、Secret Gistの場合、Content-Typeが text/plain になりますが、自作のポッドキャストプレイヤーでは問題なく取り込めました。

なお、生成するXMLについては、以下としました。

  • 不足しているエピソード分のみ生成する
    • 全部生成してしまうと、公式が配信中の内容の複製を自分で配ることになるため
  • 生成したXMLをホストして、RSSフィードの再配信をしない
    • あくまでデータの取り込みの時だけの利用とする

 

生成したXMLを既存の番組に誤マージしないようにするには

一般的なポッドキャストプレイヤーの場合、RSSフィード毎に別のポッドキャスト番組として扱われると考えています。

ただ、今回は自作ポッドキャストプレイヤーなので、既存のエピソードにマージする方法を取れば、1つの番組として作ることができそうと考えました。

そこで、自作ポッドキャストプレイヤーに既存のエピソードにインポート・マージする機能を作成しました。
https://github.com/thinkAmi/podcast-player/pull/12

 
ここで注意しなければならないのが、「セキュリティのアレではなく、他のポッドキャスト番組を指定してインポートしてしまうと、異なる番組にエピソードが混じってしまう」ことでした。

うっかりミスが発生しそうなので、何か良い方法がないかを探したところ、 RSS 2.0の source 要素がありました。
https://www.rssboard.org/rss-specification#ltsourcegtSubelementOfLtitemgt

そこで、 source 要素を使うことで、この生成したXMLはどのポッドキャストのRSSをソースとしているかを示せるようにしました。

これにより、自作ポッドキャストプレイヤー側で、XMLとインポート先が一致しているかのバリデーションが追加できました。

 

実装について

Claude Desktop の Code タブと OpenSpec を使い、OpenSpecのワークフローに従って explore から始めました。
https://github.com/Fission-AI/openspec

また、exploreの最後には最近話題になった敵対的検証を使い、穴を塞ぎました。
AIに「レビューして」はもう古い?「敵対的検証」のすすめ

 

結果

スクリーンショットの通り、無事にシーズン1の第1回以降を取り込むことができました。これで散歩中にすべての過去回を楽しめそうです。

 

2026/08/18 追記

本Blogについて、「セキュリティのアレ」のお便りコーナーで取り上げていただき、ステッカーもいただきました。ありがとうございました。
第314回 セキュリティのアレ f!スペシャル! - podcast - #セキュリティのアレ

また、お便りコーナーの中で、公式のRSSフィード件数を500件に変更した旨が紹介されていました。ご対応もありがとうございました。

そのため、2026/08/18 現在では、このツールがなくてもすべての過去エピソードがRSS配信されるようになっています。なお、後述のリポジトリのREADMEにも反映済です。

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/rss_maker_for_security_no_are

 
なお、READMEの注意事項にも書きましたが、ご利用時は特に以下についてお気をつけください。

  • セキュリティのアレまとめ様のリポジトリの構成変更などで動作しなくなる可能性があります
    • ツールについて何かあったときは、リポジトリ thinkAmi-sandbox/rss_maker_for_security_no_are のIssueへの投稿をお願いします
  • 生成したフィードは個人利用の範囲に留め、公開の場で配布しないでください
    • 公式サーバーへの負荷が高まったり、意図しない再配布になる可能性があります

Claude DesktopのSSHセッションを使って、dev containerの中でClaude Codeを動かしてみた

Claude Code CLIでは、dev containerの中で動かしたい時は、dev containerを起動して、その中でClaude Codeを起動していました。

一方、Claude DesktopのCodeタブでClaude Codeを使おうとしたところ、Codeタブからdev containerを起動することができないことに気づきました。

 
Claude Desktopでも自動承認モードなどの強い権限でClaude Codeを動かしたいこともあるので、何か良い方法がないかを調べたところ、 SSHセッション という項目がありました。
SSHセッション | Desktop application - Claude Code Docs

そこには

SSH セッションを使用すると、デスクトップアプリをインターフェイスとして使用しながら、リモートマシンで Claude Code を実行できます。これは、クラウド VM、dev コンテナ、または特定のハードウェアまたは依存関係を持つサーバーに存在するコードベースで作業するのに便利です。

と書かれており、今回の用途にマッチしてそうでした。

 
試してみたところ、「Claude Desktopにプロンプトを入力すると、dev containerの中のClaude Codeがプロンプトに従って動作した」ができたことから、メモを残します。

 
目次

 

環境

  • mac
  • Claude Desktop 1.26832.0
  • Docker Desktop
  • dev container
    • Ubuntu 26.04ベース
    • イメージ
      • mcr.microsoft.com/devcontainers/base:ubuntu26.04
    • Claude DesktopからSSH接続したときに、Claude Codeがインストールされる*1
      • そのため、dev containerのfeatureでのClaude Codeのインストールは不要

 

構築

Claude DesktopからSSH接続するための準備

今回、Claude DesktopからSSH接続するため、よくあるSSH接続同様、SSH鍵生成とssh configの記載を行います。

 

SSH鍵生成

いつもの鍵生成です。

ssh-keygen -t ed25519 -f ~/.ssh/devcontainer_projects_key -N ""

 

SSH ConfigにProxyCommandを記載

SSH接続する場合、SSH接続に使うポートを SSH Config へ設定することが多いです。

ただ、今後 dev container を利用するリポジトリが増えそうなことから、ポート指定でのSSH接続以外の方法を探しました。

 
今回は Docker を使った dev container へのSSH接続であることから、SSH の ProxyCommand を使い、 docker exec 経由でSSH接続できるようにしました。

この方法の制約として次のことがあるものの、ホストへのポート公開やポート管理が不要になったことから、採用しました。

  • SSHする時のホスト名が devcontainer- で始まること
    • 以下の仕組みを使うための制約
      • 文末の %n は、SSHがコマンドラインで受け取った元のホスト名
        • SSHが devcontainer-foo という文字列を $1 としてスクリプトに渡す
      • ${1#devcontainer-} で、受け取った $1 から devcontainer- を取り除き、foo にする
      • 後はコンテナを検索する
  • SSH Configの中に、 docker コマンドの絶対パスを書くこと
    • Claude Desktop が起動する SSH プロセスは GUI 由来の限定 PATH を引き継ぎ、docker コマンドが見つからず失敗するため
      • which docker で事前に調べたものを設定
  • リポジトリを置くパスが固定されること
    • ProxyCommand の中で固定しているため

 
今回は次の状況であるとします。

  • SSH鍵
    • ~/.ssh/devcontainer_projects_key として作成
  • which docker の結果
    • /usr/local/bin/docker
  • dev containerを含むリポジトリのありか
    • ~/devcontainer_projects/ ディレクトリの中

 
実際の設定ファイルはこちらです。

なお、 StrictHostKeyChecking no については、コンテナ再ビルドのたびにホスト鍵が変わり警告が出るため、devcontainer-*スコープに限定して無効化しています。危険なので、外部公開ホストではこの設定は避けます。

Host devcontainer-*
    HostName localhost
    User vscode
    IdentityFile ~/.ssh/devcontainer_projects_key
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null
    LogLevel ERROR
    ProxyCommand bash -c 'proj="${1#devcontainer-}"; DOCKER="/usr/local/bin/docker"; cid=$("$DOCKER" ps -q --filter "label=devcontainer.local_folder=$HOME/devcontainer_projects/$proj" --filter status=running | head -n1); if [ -z "$cid" ]; then echo "devcontainer for $proj is not running" >&2; exit 1; fi; exec "$DOCKER" exec -i "$cid" nc localhost 2222' -- %n

 

dev containerの設定

続いて、Claude DesktopからSSH接続できるよう、次のようなdev containerの設定を行います。

  • 今回は動作確認できればよいので、 Ubuntu 26.04 イメージを利用
  • featureとして、 sshd を追加
  • mountは2つ
    • SSH鍵(公開鍵)をReadOnlyでbind
    • Claude Codeの履歴などをDockerボリュームで保存
  • Claude Codeの設定ファイルの場所を環境変数 CLAUDE_CONFIG_DIR で指定
  • postCreateCommand でSSHまわりの設定を実施

 

実際の devcontainer.json はこちらです。

{
    "name": "devcontainer-cc-ssh-example",
    "image": "mcr.microsoft.com/devcontainers/base:ubuntu26.04",
    "remoteUser": "vscode",
    "features": {
        "ghcr.io/devcontainers/features/sshd:1": {}
    },
    "mounts": [
        "source=${localEnv:HOME}/.ssh/devcontainer_projects_key.pub,target=/tmp/devcontainer_projects_key.pub,type=bind,readonly",
        "source=claude-code-config-${devcontainerId},target=/home/vscode/.claude,type=volume"
    ],
    "containerEnv": {
        "CLAUDE_CONFIG_DIR": "/home/vscode/.claude"
    },
    "postCreateCommand": "sudo apt-get update && sudo apt-get install -y netcat-openbsd && mkdir -p ~/.ssh && cat /tmp/devcontainer_projects_key.pub >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys && sudo chown -R $(whoami) ~/.claude"
}

 

dev container の起動

ここまでで設定が終わったため、VS Codeなどでこのdev container devcontainer-cc-ssh-example を起動しておきます。

 

Claude DesktopでClaudeの実行場所を設定

最後に Claude Desktop に設定を行います。

まずは、Claudeの実行場所 > SSH > SSHホストを追加 の順にクリックします。

 
次に、以下の内容を入力して SSH接続を追加 をクリックします。

ここで注意するところはSSHホストです。ProxyCommandで設定した通り、「 ~/devcontainer_projects/直下のディレクトリ名」とします。

項目
名前 任意の値 (今回は claude_code_ssh_example)
SSHホスト devcontainer-claude_code_ssh_example
SSHポート 空白
IDファイル 空白

 
しばらくすると接続が完了します。

 
続いて リモートフォルダを参照 をクリックし、Claude Code を動かす dev container 上のディレクトリを選択します。

デフォルトだとユーザー (今回は vscode )のホームディレクトリになりますが、そこでは Claude Code は動きません。

そこで、VS Code にて dev container を起動したときに移動しているワークスペース /workspaces/<ディレクトリ名> を選択します。

今回の場合は /workspaces/claude_code_ssh_example を選択します。

 

動作確認

ここまでで準備ができたので、動作確認します。

まずは !pwd したところ、 /workspaces/claude_code_ssh_example が返ってきました。

 
次に 現在動かしている環境と、現在のディレクトリには何があるかを教えてください と確認したところ、dev container の中で動いていることが分かりました。

 
最後に、Claude Codeにファイルを作成してもらいました。

 
VS Codeで確認すると、確かにファイルができていました。

 

ソースコード

GitHub に上げました。
https://github.com/thinkAmi-sandbox/claude_code_ssh_example

*1:公式ドキュメントによると「デスクトップは初回接続時にリモートマシンに Claude Code を自動的にインストールします」とのこと

自分のAndroidスマホ向けに、ポッドキャストプレイヤーを作ってみた

最近、外出をするときにポッドキャストを聴くことが多くなりました。

今まで利用していたアプリは多機能な一方、それらを使いこなせていない感がありました。

そこで、自分のユースケースにあったポッドキャストプレイヤーを作ってみたことから、メモを残します。

 
目次

 

機能について

自分の場合、事前に家でポッドキャストのエピソードをダウンロードしておいて、外出時は再生するだけでした。そのため、以下の機能があれば良さそうでした。

  • ポッドキャストの管理
    • RSSページを元にした登録
    • 音声データのダウンロード
  • エピソードの管理
    • 再生、停止
    • RSSの購読によるエピソード登録
    • いわゆる既読・未読管理
    • 聴き終わった後の音声ファイル削除
    • エピソードの絞り込み

 
一方、自分のユースケースに合わない機能は実装しませんでした。

  • ストリーミング再生
  • エピソードの自動更新や、音声ファイルの自動ダウンロード
  • ポッドキャスト検索

 

技術スタックについて

個人のAndroid端末だけで動作確認が取れればよいので、Android 16で動くものを優先しました。

また、できる限り外部ライブラリへの依存を減らすことでアップデートを容易にしたかったことから、プロダクションコードはGoogleとJetBrains製のライブラリのみにしました。

分類 ライブラリ バージョン
言語 Kotlin 2.3.10
UI Jetpack Compose(BOM) 2026.06.01
再生 Media3(ExoPlayer + MediaSession + MediaSessionService) 1.10.1
DB Room 2.8.4
非同期 kotlinx-coroutines 1.11.0
HTTP HttpURLConnection (OS 標準)
RSS パース XmlPullParser (OS 標準)
画像 BitmapFactory + 自前ファイルキャッシュ

 

今まで音声ファイルを扱ったことがなかったのでライブラリ選定が難しそうだと思っていました。しかし、 androidx/media を使えば今回必要な機能がすべて含まれていると分かり、だいぶ簡潔になりました。
https://github.com/androidx/media

 
一方、テストコードやリンターなどについては、AGPやKoverの他に、サードパーティライブラリも導入しました。ビルド時はサードパーティを許容した感じです。

ツール 役割 バージョン
AGP ビルド 9.3.0
ktfmt フォーマッター 0.64
detekt 静的解析 2.0.0-alpha.5
Kover カバレッジ 0.9.9
JUnit 4 テストランナー 4.13.2
kotest-property プロパティベースドテスト(property モジュールのみ) 6.2.3

 
プロパティベースドテストのライブラリについては、最初、以下を眺めて選定を考えました。
https://github.com/jmid/pbt-frameworks

しかし、Java/Kotlinのライブラリについては更新が止まってそうなものもあり、どれを採用するか迷いました。

そんな中、kotestにプロパティベースドテストの機能が含まれていると知りました。

 
まずはこれで十分だろうということで、 kotest-property だけを導入しました。そのため、テストランナーや書き方については、JUnit 4ベースのままになっています。

 
また、detektはアルファ版であっても2系にした理由は以下です。

  • 1系はKotlin2.3系に未対応
    • 2系は対応、今後の安定版に期待
  • この規模のアプリなら問題が出ても大したことがないだろう
  • ビルド時のみの依存であり、プロダクションコードには影響しないこと

 

開発〜運用について

2026年7月現在の開発方法のスナップショットとしてメモしておきます。

 

仕様の詰め方について

Fable5 high + OpenSpec + grill-me (grilling) で詰めました。流れは以下でした。

  • OpenSpecの /opsx:explore で仕様を発散
  • grill-meで仕様を収束
  • OpenSpecの /opsx:ff でドキュメントづくり

 
なお、機能があれば良いということで、UI/UXについてはモデル任せにしました。

 

実装について

AIエージェントの方が Android + Kotlin に詳しいため、実装は任せることにしました。

Fable5 high もしくは Opus 4.8 high を使い、 /opsx:apply で実装してもらいました。

なお、自分で実装しない分、

  • リンター
  • フォーマッター
  • テストコード
    • 事例ベーステスト
    • プロパティベースドテスト
  • テストカバレッジ

などを整備しました。

detektを使って「例外の握りつぶし」「アーキテクチャ違反」などが発生した場合はビルドエラーにすることで、ある程度の品質を担保しています。

また、AndroidのテストまわりについてはAIエージェントの方が詳しいので、より良いテスト手法があればそれを採用しました。

 

運用について

デプロイについては、macとスマホをUSBケーブルで繋いでインストールすることにしました。

また、スマホでポッドキャストを登録するのがやや手間なので、AIスキル「USBケーブルを介してPCからポッドキャストを登録する」を用意しました。

 

スクリーンショット

AIによるUI/UXのまま、特に手を加えていない状態になっています。

 

トップ

 

ポッドキャストの詳細

 

再生中の画面

 
 

おわりに

シンプルですが、自分のほしかった機能は手に入ったので良しとします。

後は使ってみて、気になったところを修正していければと考えています。

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi/podcast-player

ObsidianでToDoを管理できるobsidian-kanban-flowプラグインを作ってみた

最近、マルチタスクになることが多いことから、ToDo管理をすることにしました。

何かローカルで動くToDo管理ツールがないかを探したところ、Obsidianのプラグインである obsidian-kanban がありました。
https://github.com/obsidian-community/obsidian-kanban

 
これでしばらく運用してみたところ、次のようなことを感じました。

  • 少しカスタマイズしたくなった
    • いつ終わったかを記録したい
    • 月単位でタスクをアーカイブしたい
  • プラグインのメンテナンスが落ち着いている
    • 最終リリースが2024/5/31
    • リポジトリのREADMEにも「The Kanban plugin is looking for new maintainers.」と書かれている

 
ToDoの状況をmarkdownで管理するというアーキテクチャは好みだったことから、Obsidianの他のプラグインを探すことも考えました。

しかし、今ならAIとともに作れそうと感じてAIとともに作ってみたことから、メモを残します。

 
目次

 

環境

  • mac
  • Obsidian
  • Claude Code 2.1.195
  • Opus 4.8 high

 

機能について

obsidian-kanban プラグインのソースコードを確認したところ、自分の使い方としてはオーバースペックのように感じました。

そこで、機能をすべて再現するのではなく、次のような、自分のほしい必要最低限の機能に絞ったプラグインを作ることにしました。

  • レーンは固定
  • カードの追加と削除は可能
  • カードには作成日と完了日をもたせる
  • カードのレーン間移動はドラッグアンドドロップ
  • アーカイブは、完了レーンのカードのみ可能
  • デザインは最小限

 

技術スタックについて

せっかくなので、自分の知っている技術スタックで作るのではなく、今まで使ったことがなかったり、最近目にする技術スタックを採用しました。

一方、CIまわりは、自作のRailroadsプラグインのものをベースに、今回の要件に合わせて調整しました。
https://github.com/thinkAmi/railroads

 
なお、ToDoのデータをmarkdownで管理するというアーキテクチャは、 obsidian-kanbanと同じです。

このアーキテクチャの場合、ローカルデータが吹っ飛んだときは問題が発生しそうですが、タスクの詳細な内容については別のところに記載していることから、今回は問題ないと判断しました。

 

プラグインの技術基盤

obsidian-kanbanではPreactを使っていました。

今回機能を絞ったところ、obsidian-kanbanのようなPreactではなく、Svelteでも作れそうでした。

そこで、obsidian-kanbanをフォークして機能を追加するのではなく、Svelteでイチから作ることにしました。

Svelteだと、ドラッグアンドドロップのライブラリ svelte-dnd-action があるので採用しやすかったです。
https://github.com/isaacHagoel/svelte-dnd-action

 

OxlintやOxfmtによるLint・Format

AIに任せることもあり、

  • Oxlint
  • Oxfmt

を導入しました。

 

Vitestとfast-checkによるテスト

テストランナーとして Vitest を採用しました。

また、今回、markdownの処理を自作のパーサーで行っています。そのため、事例ベーステストに加え、プロパティベーステストでもテストを書きたくなりました。

JS/TSの場合、プロパティベーステストは fast-check 一択のように見えたことから、今回は fast-check を導入しています。
jmid/pbt-frameworks: An overview of property-based testing functionality

ちなみに、プロパティベーステストを導入したことで、分岐カバレッジが上がりました。

 

CIでのビルド、配布、テスト実行

Obsidianプラグインを作る場合、 main.jsmanifest.json の2つは、最低限配布する必要があります。今回、それに加えてスタイルを適用する styles.css も配布します。

一方、プラグインの最低動作バージョンを示す versions.json はリポジトリに用意しますが、配布までは行いません。

今回はGitHub Actionsを使い、GitHubのReleaseでそれら3つのファイルを配布できるようにしています。

あとは、プラグインのバージョンを上げる作業やリリース向けの作業については、Claude CodeのAIスキルとして用意しました。

 
また、CIでは typecheck や lint 、テストも行うようにしています。

 

スクリーンショット

全体のビューはこんな感じです。各カラムの一番下でカードを追加できます。

 
カード単位での削除も可能です。

 
DONEレーンのみ、DONEにあるカードを一括でアーカイブできます。アーカイブ時は年月ごとにカードを振り分けます。

 

データはmarkdownとして保存しています。そのため、markdownファイルとして開くと次のように見えます。

 

おわりに

自分の欲しかったToDo管理ツールを作れました。

また、以下の点も良かったです。

  • Obsidianのプラグインを作るには何が必要なのかを学べた
  • 1つのmarkdownファイルでToDoリストを管理しているため、AIに分析させるのも容易になった
  • 今まで使ったことのない技術スタックのソースコードが入手できた

 
使っていて気になるところが出てきたら、順次改善していこうと思います。

 

ソースコード

GitHubに上げています。
https://github.com/thinkAmi-sandbox/obsidian-kanban-flow

ShredOS x86_64 - Disk Eraserを使って、Windows PCのディスクを消去してみた

最近、PCをリサイクルに出す機会がありました。

ディスクの中身を消去して良いとのことだったため、ディスク消去ツールを探してみたところ、shredos.x86_64がありました。
https://github.com/PartialVolume/shredos.x86_64

そこで、使ってみた時のメモを残します。

 
目次

 

環境

  • 削除対象PC
    • Windows10
      • やや古いマシン
  • 空のUSBメモリ
  • ShredOS x86_64 v2025.11_29_x86-64_0.40
    • .img / BIOS+UEFI USB / minimum 2GB RAM / writable
    • macでUSBメモリに書き込んで、Windows10のマシンでUSBブート

 

macにて、ShredOS x86_64をUSBメモリへ書き込み

USBメモリをmacへ接続し、 diskutil list でUSBメモリが認識されることを確認します。今回は disk5 として認識されていました。

% diskutil list
...
/dev/disk5 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *62.0 GB    disk5
   1:               Windows_NTFS KIOXIA                  62.0 GB    disk5s1

 
続いてイメージをダウンロードします。

今回はmacでUSBへ書き込むことから x86-64 .img / BIOS+UEFI USB / minimum 2GB RAM / writable である shredos-2025.11_29_x86-64_v0.40_20260402.img をダウンロードしました。

なお、ShredOSのREADMEには圧縮されているような記述もありましたが、今回は img ファイルそのものをダウンロードできました。

 
ダウンロード後、ファイルのチェックサムを確認します。

チェックサムはReleaseページの内容と一致していたため、良さそうでした。

% shasum /path/to/shredos-2025.11_29_x86-64_v0.40_20260402.img 
fba1bc6c383923a3e70ab741924079752ecaa0b0  /path/to/shredos-2025.11_29_x86-64_v0.40_20260402.img

 
続いて dd でUSBメモリへ書き込もうとしましたが、busyでした。

% sudo dd if=/path/to/shredos-2025.11_29_x86-64_v0.40_20260402.img of=/dev/disk5
Password:

dd: /dev/disk5: Resource busy

 
そこで、 diskutil unmountDisk でアンマウントします。

% diskutil unmountDisk /dev/disk5
Unmount of all volumes on disk5 was successful

 
再度 dd で書き込んだところ、書き込みができました。

% sudo dd if=/path/to/shredos-2025.11_29_x86-64_v0.40_20260402.img of=/dev/disk5
732159+1 records in
732159+1 records out
374865664 bytes transferred in 110.527438 secs (3391607 bytes/sec)

 
念のため、 sync もしておきます。

% sync

 
書き込みが完了したので、 diskutil list で状況を確認します。Typeが変わっているなど、書き込みは成功したようです。

% diskutil list
...
/dev/disk5 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *62.0 GB    disk5
   1:             Windows_FAT_32 NO NAME                 374.2 MB   disk5s1
                    (free space)                         61.6 GB    -

 
続いて、USBメモリを取り外すために、eject します。

% diskutil eject /dev/disk5
Disk /dev/disk5 ejected

 
念のため、 diskutil list して、リストからなくなったことを確認します。

% diskutil list

 
最後に、USBメモリを取り外します。

 

ShredOS x86_64によるディスク消去

WindowsマシンにUSBメモリを接続し、USBメモリからブートします。

ALT-F2 でターミナル入力できるようにします。

 
続いて、データの消去を行います。まずは -s 2 で試してみます。なお、namespaceは1つしかなかったため、 -n 1 としています。

実行したところ、エラーで消去できなかったようです。

$ nvme format /dev/nvme0 -s 2 -n 1
...
NVMe status: Invalid Field in Command A reserved coded value or unsupported value in a defined field(0x2)

 
暗号消去に対応しているか確認したところ、何も表示されませんでした。今回のWindows PCのディスクが対応していないようです。

$ nvme id-ctrl /dev/nvme0 | grep -i crypto

 
そのため、 -s 1 (全ユーザーデータの削除) とすることにしました。

$ nvme format /dev/nvme0 -s 1 -n 1

 
時間もかからずに終了しました。

sanitize も実行しようと考えましたが、対応していなかったので実行できませんでした。

 
これで作業は完了です。 poweroff します。

$ poweroff

 
電源が落ちた後、USBメモリを抜きました。

 

参考

JetBrains IDEのプラグインRailroadsにて、利用しているGitHub Actionsをpinactでコミットハッシュ固定してみた

Railroadsプラグインでは、複数のGitHub Actionsを利用しています。

GitHub Actionsは、ベースとなったJetBrainsのIntelliJ Platform Plugin Templateと同様に、バージョンで固定していました。

 
そんな中、最近の

  • 本当ならバージョンではなく、コミットハッシュで固定したほうが良いこと
  • IntelliJ Platform Plugin Templateとの差分はAIに検知してもらえそうなので、以前に比べるとコミットハッシュでの固定も容易であること

という状況より、GitHub Actionsはコミットハッシュで固定したくなりました。

 
ただ、バージョンからコミットハッシュへの変換を手動でやるのはさすがに手間です。ツールを探してみたところ、 pinact がありました。
https://github.com/suzuki-shunsuke/pinact

 
そこで、Railroadsで利用しているGitHub Actionsに対し、pinactでコミットハッシュ固定してみたことから、メモを残します。

 
目次

 

環境

  • pinact 3.10.1
  • mise 2026.5.10 macos-arm64 (2026-05-16)
  • pinact action 2.0.0

 

pinactの使用

まずは pinact を使って、コミットハッシュで固定してみます。

 

pinactをインストール

pinact のドキュメントを読むといくつか方法があります。
https://github.com/suzuki-shunsuke/pinact/blob/main/INSTALL.md

Homebrewでのインストールがお手軽ですが、 aqua でもインストールできます。インストールするバージョンの固定やチェックサムの検証ができるのは良さそうです。
https://github.com/aquaproj/aqua

また、 mise でもインストールできます。miseの場合、aquaバックエンドでのインストールになるようです。ふだんmiseを使っていることもあり、自分の用途だとmiseでのインストールで十分そうです。
https://mise.jdx.dev/dev-tools/backends/aqua.html

 
そこで、miseのaquaバックエンドでpinactをインストールします。

なお、miseはデフォルトでaquaバックエンドを使っており、 pinact もaquaで管理されていることから、 aqua: はなくても良さそうです。

% mise use --pin aqua:suzuki-shunsuke/pinact@latest
aqua:suzuki-shunsuke/pinact@3.10.1 extract pinact_darwin_arm64.tar.gz                                                                                                                                                                                                       ✔
mise ~/project/railroads/mise.toml tools: aqua:suzuki-shunsuke/pinact@3.10.1

 
実行後、設定ファイル (mise.toml) に pinact の設定が追加されたことを確認します。

[tools]
"aqua:suzuki-shunsuke/pinact" = "3.10.1"

 

minimum_release_age の設定を追加

最近の状況だと、リリースされたばかりのツールをインストールするのは危険なこともあります。そこで、miseの minimum_release_age を使ってリリース後7日経過してからインストールするようにします。
https://mise.jdx.dev/configuration/settings.html#minimum_release_age

 
具体的には、リポジトリの mise.toml に以下の記述を追加します。

[settings]
minimum_release_age = "7d"

 

GitHub Actionsのバージョンを上げる

pinact の準備ができたので、次はGitHub Actionsのバージョンを上げます。

このタイミングでは actions/checkout@v6.0.2 のように、バージョンで指定します。

 

pinactでバージョンをコミットハッシュへと変換

pinact run すると、バージョンがコミットハッシュへと変換されます。

また、次のようにコミットハッシュのところにコメントでバージョンも記載されるため、このコミットハッシュはどのバージョンを指しているのかがわかりやすいです。

actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

 

pinact化を強制するワークフローを追加

pinact run の実行忘れがありそうだったため、pinact化を強制するワークフローを追加しました。

また、GitHub Actionsのrulesetへ追加し、このワークフローが失敗した場合はプルリクをマージできないようにします。

 

ワークフロー

pinact公式の pinact-action を使ったワークフローを用意します。
https://github.com/suzuki-shunsuke/pinact-action

今回はチェックだけにとどめたいため、 skip_push: trueverify: true を設定しています。

なお、このファイルも pinact run し、GitHub ActionsのSHAを固定しておきます。

name: Pin GitHub Actions

on:
  pull_request:
  push:
    branches: [ main ]

permissions:
  contents: read

jobs:
  pinact:
    name: pinact
    runs-on: ubuntu-latest
    steps:
      - name: Fetch Sources
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: Check pinned GitHub Actions
        uses: suzuki-shunsuke/pinact-action@cf51507d80d4d6522a07348e3d58790290eaf0b6 # v2.0.0
        with:
          skip_push: "true"
          verify: "true"

 

GitHubのRulesetsへの追加

Settings > Rules > Rulesets より追加します。

Railroadsではすでに main branch protection ルールを設定していたため、それを更新することにします。

Require status checks to passpinact ワークフローを追加します。

 
あとは、実際に動かしてみて、 pinact ワークフローが動作することを確認しました。

 

ソースコード

対応した時のプルリクはこちらです。
https://github.com/thinkAmi/railroads/pull/108

 

参考

miseでツールのバージョン管理をすることを何度でも推したい(2026年版) | DevelopersIO