YAPC::Fukuoka 2025で「読む技術・書く技術・伝える技術 - 15年続けて分かった持続可能なオープンソース開発」というタイトルで発表をしました。
スライドは次のページで公開しています。
この発表では、15年間のオープンソース活動から学んだ持続可能な開発のための3つのプロジェクトについて話しています。
JSer.infoは2011年から続けている週刊のJavaScript情報ブログで、これまでに750記事以上を公開しています。 「整理されたデータである『情報』を伝えること」をテーマに、14年間続けてきた情報収集システムやワークフローについて紹介しています。
関連記事:
textlintは、自然言語のLintツールで、2014年から開発を続けています。 現在では200以上のルールを持つエコシステムになっており、AI時代における文章品質の自動化についても触れています。
関連記事:
JavaScript Primerは、2016年から開発を続けているJavaScriptの入門書です。 「変化に対応できること」をテーマに、最初からLiving Standard戦略や長期的な運用を目的として設計されました。 100人以上のコントリビューターが参加しており、どのように継続的なメンテナンスとコミュニティの参加を実現しているかについて話しています。
関連記事:
発表のテーマは「心理的負荷を技術的依存に転換する」という考え方です。
バーンアウトは期待と現実のギャップから生じます。心理的負荷はコントロールが難しく、バーンアウトのリスクを高めます。 一方で、技術的依存は自動化が可能で、継続することで改善が加速し、コントロールがしやすいです。

The End of Burnout by Jonathan Malesic - Paper - University of California Press
そもそも持続可能性を気にしているのは、それぞれのプロジェクトがアウトプット(作ったもの)ではなくアウトカム(実際の影響・信頼・教育)を目指してやっているからです。長期的な視点でプロジェクトを継続するにコントロールが難しい心理的負荷を意識的に減らす工夫について話しました。
今回のスライドは、Claude Desktopで「こんなテーマにしたい、過去の自分の公開したものとかを調べてまとめてみて」という形でDeep Researchした結果を元に構成しました。 その後、大まかなスライドで発表練習をして構造的な問題もしゃべりながら音声ファイルを文字起こしし、それを元にスライドを修正するというのを2回ぐらいやって構造を固めました。(若干テーマが持続的開発に変わったのはこの辺のイテレーションで変わった)
構造が固まったら、Claude Desktopのプロジェクトをファイルにdumpして、Claude Codeでスライドを書いていくというフローで作成しました。
今回のスライドはClaude Desktopで「こんなテーマにしたい、過去の自分の公開したものとかを調べてまとめてみて」って感じの雑なDeepResearchした結果を元に数コを合成したスライドにして、発表練習して、音声ファイルを文字起こしを元にスライドを直して、2週ぐらいしてからClaude Codeで細かく直した pic.twitter.com/a3oVm6GSFD
— azu (@azu_re) November 15, 2025
この辺は、過去にPublicに書いてきたことをAIが読んである程度書いたものを人間が整理して伝えるということができて結構面白かったです。
JSer.infoは趣味の延長として始まったプロジェクトですが、textlintやJavaScript Primerは最初から長期的な運用を見据えて設計しました。 それぞれのプロジェクトが9年や11年や14年と続けてこられたのは、持続可能性を意識した設計と、コミュニティの支援があったからです。
スライドのAppendixに大量の参考リンクとかがあるので、興味ある人はぜひ見てみてください。
オープンソースの活動は色々な形があるので、JSer.info、textlint、jsprimerに限らず、何かしらの形で関わってみると面白いかもしれません。
GitHub Sponsorsでの支援も大歓迎です!
YAPC::Fukuoka 2025 お疲れ様でした!
]]>npm/yarn/pnpm/bunを同じコマンドで扱えるni.zshのv1.8.0をリリースしました。
このバージョンでは、Socket Firewallを統合し、パッケージのインストールと実行時にサプライチェーン攻撃から保護する機能を追加しました。
ni.zshについては、次の記事を参照してください。
主要な変更点は次の通りです。詳細はリリースノートを参照してください。
Socket Firewallを統合し、パッケージのインストール時にセキュリティスキャンを行えるようになりました。
Socket Firewallは、HTTPプロキシとしてパッケージマネージャのネットワーク通信をインターセプトし、悪意のあるパッケージがダウンロードされる前にブロックするツールです。
従来のni.zshでは、Socket.dev APIを使ってリスクスコアを取得し、インストール前に確認を求める仕組みでした。 Socket Firewallも基本的には同じで、自動的にインストール時にマルウェアならブロックし、潜在的なマルウェアは警告を表示します。
詳しくは次の記事を参照してください。
Socket Firewallの機能を使用するには、次の手順でセットアップします。
npm i -g sfw
.zshrc に環境変数を追加export NI_USE_SOCKET_FIREWALL=1
設定後、次のコマンドでSocket Firewallによる保護が有効になります。
次のコマンドでSocket Firewallによる保護が有効になります。
ni / ni add <pkg> - パッケージのインストールni exec <cmd> / ni dlx <pkg> - パッケージの実行ni.zshでは、パッケージをインストールする可能性があるコマンドのみを選択的にSocket Firewallを通すように実装されています。これにより、alias npm="sfw npm"のような全コマンドをプロキシする方法と比べて、余計なSocket Firewallのログが出力されず、必要な時だけ保護機能が動作します。
# パッケージインストール時に保護
ni add express
# 直接実行する場合も保護
ni dlx create-vite
マルウェアパッケージがブロックされると、次のように表示されます。
=== Socket Firewall ===
- Blocked npm package: name: lodahs; version: 1.0.0
デフォルトではsfwコマンドを使用しますが、カスタムのSocket Firewallバイナリパスを指定できます。
export NI_SOCKET_FIREWALL_BIN=/path/to/custom/sfw
最近のnpmエコシステムでは、サプライチェーン攻撃が急増しています。
過去にはevent-stream攻撃のような稀な事例でしたが、2024年から2025年にかけて次のような有名パッケージの侵害が相次いで発生しています。
これらの攻撃では、メンテナのアカウントが乗っ取られたり、マルウェアが連鎖的にマルウェアとなるnpmパッケージを公開しています。
Socket Firewallは、HTTPプロキシとしてネットワーク通信をインターセプトし、既知のマルウェアパッケージを自動的にブロックします。
これはnpmパッケージ/GitHub Actionsを利用する側/公開する側でサプライチェーン攻撃を防ぐためにやることメモで書いていた多層防御の一環として位置づけられます。
Socket Firewallは、この多層防御の「事前スキャン」段階の保護をするイメージです。
ni.zshは複数のパッケージマネージャ(npm、yarn、pnpm、bun)をラップするため、どのパッケージマネージャを使用していてもSocket Firewallによる保護を受けられます。
次のようにそれぞれのパッケージマネージャをsfwでラップするエイリアスするのと大体似たような動作になります。
alias yarn="sfw yarn"
alias pnpm="sfw pnpm"
alias npm="sfw npm"
alias bun="sfw bun"
alias npx="sfw npx"
ni.zshでは、以前からSocket.dev APIを使ったマルウェア検知機能を提供していました。
Socket.dev APIは、パッケージのリスクスコアを取得して、インストール前に確認を求める仕組みでした。
基本的な機能性は同じですが、Socket Firewallは次のような違いがあります。
| 機能 | Socket.dev API | Socket Firewall |
|---|---|---|
| 検知方法 | APIでリスクスコアを取得 | HTTPプロキシでネットワーク通信をインターセプト |
| 保護レベル | インストール前に確認を求める | 既知のマルウェアは自動的にブロック |
| セットアップ | APIキーが必要 | APIキー不要(無料) |
| 対応パッケージマネージャ | npm/yarn/pnpm/bun | npm/yarn/pnpm/pip/uv/cargo |
Socket Firewallは無料で使えて、APIキーも不要なため、よりシンプルに使えるようになりました。
なお、従来のSocket.dev API機能は削除しました。警告が出ても意識的にチェックしない場合が多く、ブロックするか通すかの明確な判断の方が実用的だと考えたためです。もしSocket Firewallが間違ってパッケージをブロックした場合は、直接npmやyarnなどのコマンドを使用することで回避できます。
ni.zsh v1.8.0では、Socket Firewallを統合してパッケージのセキュリティ保護機能を追加しました。
Socket Firewallを有効にするには、sfwをインストールしてNI_USE_SOCKET_FIREWALL=1を設定するだけです。
フィードバックがありましたら、GitHubのIssueでお知らせください。
]]>npm Trusted Publishingが2025年7月31日に一般公開されました。 これにより、OpenID Connect (OIDC)を使ってnpmトークンなしでCI/CDからnpmパッケージを公開できるようになりました。
この記事では、npm Trusted Publishingの仕組みや設定方法、実際のリリースフローについて紹介します。
npm Trusted Publishingは、npmレジストリとCI/CD環境(GitHub ActionsやGitLab CI/CD)の間でOIDCベースの信頼関係を確立する仕組みです。これにより、npmトークンを使わずにパッケージを公開できます。
これまでのCIからnpmパッケージを公開する際には、長期間有効なnpmトークンをCI/CDの環境変数(Secrets)に保存する必要がありました。しかし、このアプローチにはいくつかのセキュリティリスクがあります。
npm Trusted Publishingは、これらの問題を解決します。 短時間だけ有効で、かつ特定ワークフローに限定された署名付きトークンを使うため、流出しても再利用されにくい構造になっています。
現在、npm Trusted Publishingは次のCI/CD環境をサポートしています。
npm Trusted Publishingの設定は大きく2つのステップで構成されます。
まず、npmjs.comのパッケージ設定でTrusted Publisherを設定します。
GitHub Actionsの場合は次のような設定をします。
publish.yml)
.ymlまたは.yaml拡張子を含める必要がある.github/workflows/内に存在する必要があるワークフローにOIDC権限 id-token: write を追加します。例えば、次のようなワークフローを設定すると、タグをプッシュしたときにnpmパッケージを公開できます。
name: Publish Package
on:
push:
tags:
- 'v*'
permissions:
id-token: write # OIDCに必要
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
# npm 11.5.1以降が必要
- name: Update npm
run: npm install -g npm@latest
- run: npm ci
- run: npm run build --if-present
- run: npm test
- run: npm publish
重要なのはid-token: write権限とnpm 11.5.1以降を使用することです。これにより、GitHub Actionsがnpmに対して一時的な署名付きトークンを発行できるようになります。
:memo: pnpmのpnpm publishコマンドはnpm publishに処理を移譲しているため、pnpmを使う場合もnpm 11.5.1以降が必要です。
自分が実際に運用している単独パッケージとmonorepoパッケージでのリリースフローを紹介します。
新規パッケージでは、OIDCはパッケージが一度公開されていないと設定できません。 そこで、この問題を解決するためにsetup-npm-trusted-publishというツールを作成しました。
READMEのみを含む初期パッケージを一度公開し、OIDC設定をするための下準備を自動化するツールです。 次のコマンドでパッケージを公開できるので、作成したパッケージにTrusted Publisherを手動で行います。
npx setup-npm-trusted-publish <パッケージ名>
実際のパッケージ例。
初めての公開後に、実際にCIからのPublishができるようになります。
すでに一度でも公開済みであれば、そのパッケージのnpmjs.com設定画面から直接Trusted Publisherを追加できます。初期公開用の空パッケージを別途用意する手順は不要です。
実運用では次の2つのWorkflowに分けています。
create-release-pr.yml: リリースPRを作るだけ(publishしない)release.yml: リリースPRがマージされたら publish して GitHub Release を作る。npm 側の Trusted Publisher にはこの release.yml だけを登録する。「PRでリリースノートを確認/編集 → マージで公開」というシンプルな二段構成です。
npmjs.comのTrusted Publisher設定では release.yml のみを指定します。create-release-pr.yml は公開権限を持たないリリースPRを作るだけのワークフローです。
そのため、user/example-pkgリポジトリで @user/example-pkgのTrusted Publisher設定は次のようになります。
https://www.npmjs.com/package/@user/example-pkg/settings/accessuser を設定example-pkg を設定release.yml を設定Trusted Publisherの設定はnpmjs.comでの手動の作業が必要です。 手動設定でのミスを防ぐため、1Passwordのautofill機能を活用しています。
opコマンドで生成しておく)Trusted Publisherの設定は項目が多く間違いやすく、設定ミスを減らすことが目的です。 手入力をなくすことで、設定ミスを減らせます。
OIDC連携するためのnpm trusted publishの設定の作業の様子です。 pic.twitter.com/2kRXto3Z45
— azu (@azu_re) September 6, 2025
create-release-pr.ymlは手動でリリースPRを作成するワークフローです。
実行時に、アップデートするバージョンの種類(patch / minor / major)を選択します。
次のようなステップで動作します。
npm version 1.2.3 --no-git-tag-version 相当)releases/generate-notes API で差分ベースのリリースノートを生成release/vX.Y.Z ブランチを作成し package.json (+ lockfile) をコミットType: Release)create-release-pr.ymlは次のようなことを考慮しています。
create-release-pr.yml は次のような内容です。
name: Create Release PR
on:
workflow_dispatch:
inputs:
version:
description: 'Version type'
required: true
type: choice
options:
- patch
- minor
- major
jobs:
create-release-pr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 'lts/*'
# No need to install dependencies - npm version works without them
- name: Version bump
id: version
run: |
npm version "$VERSION_TYPE" --no-git-tag-version
VERSION=$(jq -r '.version' package.json)
echo "version=$VERSION" >> $GITHUB_OUTPUT
env:
VERSION_TYPE: ${{ github.event.inputs.version }}
- name: Get release notes
id: release-notes
run: |
# Get the default branch
DEFAULT_BRANCH=$(gh api "repos/$GITHUB_REPOSITORY" --jq '.default_branch')
# Get the latest release tag using GitHub API
# Use the exit code to determine if a release exists
if LAST_TAG=$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq '.tag_name' 2>/dev/null); then
echo "Previous release found: $LAST_TAG"
else
LAST_TAG=""
echo "No previous releases found - this will be the first release"
fi
# Generate release notes - only include previous_tag_name if we have a valid previous tag
echo "Generating release notes for tag: v$VERSION"
if [ -n "$LAST_TAG" ]; then
echo "Using previous tag: $LAST_TAG"
RELEASE_NOTES=$(gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
"/repos/$GITHUB_REPOSITORY/releases/generate-notes" \
-f "tag_name=v$VERSION" \
-f "target_commitish=$DEFAULT_BRANCH" \
-f "previous_tag_name=$LAST_TAG" \
--jq '.body')
else
echo "Generating notes from all commits"
RELEASE_NOTES=$(gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
"/repos/$GITHUB_REPOSITORY/releases/generate-notes" \
-f "tag_name=v$VERSION" \
-f "target_commitish=$DEFAULT_BRANCH" \
--jq '.body')
fi
# Set release notes as environment variable
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$RELEASE_NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.version }}
GITHUB_REPOSITORY: ${{ github.repository }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
branch: release/v${{ steps.version.outputs.version }}
delete-branch: true
title: "Release v${{ steps.version.outputs.version }}"
body: |
${{ env.RELEASE_NOTES }}
commit-message: "chore: release v${{ steps.version.outputs.version }}"
labels: |
Type: Release
assignees: ${{ github.actor }}
draft: true
release.yml の流れrelease.ymlはリリースPRがマージされたら自動でnpm publishを実行するワークフローです。
release.ymlは次のようなステップで動作します。
Type: Releaseラベルがあるかを確認package.json の version からタグ名 vX.Y.Z を決定npm publish --provenance (npm 11.5.1+ が必要)release.yml は次のような内容です。
name: Release
on:
pull_request:
branches:
- master
- main
types:
- closed
jobs:
release:
if: |
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'Type: Release')
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write # OIDC
pull-requests: write # PR comment
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Get package info
id: package
run: |
VERSION=$(jq -r '.version' package.json)
PACKAGE_NAME=$(jq -r '.name' package.json)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "name=$PACKAGE_NAME" >> $GITHUB_OUTPUT
- name: Check if tag exists
id: tag-check
run: |
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
env:
VERSION: ${{ steps.package.outputs.version }}
- name: Setup Node.js
if: steps.tag-check.outputs.exists == 'false'
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 'lts/*'
registry-url: 'https://registry.npmjs.org'
- name: Install latest npm
if: steps.tag-check.outputs.exists == 'false'
run: |
echo "Current npm version: $(npm -v)"
npm install -g npm@latest
echo "Updated npm version: $(npm -v)"
- name: Install dependencies
if: steps.tag-check.outputs.exists == 'false'
run: npm ci
- name: Publish to npm with provenance
if: steps.tag-check.outputs.exists == 'false'
run: npm publish --provenance --access public
- name: Create GitHub Release with tag
id: create-release
if: steps.tag-check.outputs.exists == 'false'
run: |
RELEASE_URL=$(gh release create "v$VERSION" \
--title "v$VERSION" \
--target "$SHA" \
--notes "$PR_BODY")
echo "url=$RELEASE_URL" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.package.outputs.version }}
SHA: ${{ github.sha }}
PR_BODY: ${{ github.event.pull_request.body }}
- name: Comment on PR - Success
if: |
always() &&
github.event_name == 'pull_request' &&
steps.tag-check.outputs.exists == 'false' &&
success()
run: |
gh pr comment "$PR_NUMBER" \
--body "✅ **Release v$VERSION completed successfully!**
- 📦 npm package: https://www.npmjs.com/package/$PACKAGE_NAME/v/$VERSION
- 🏷️ GitHub Release: $RELEASE_URL
- 🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID"
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
VERSION: ${{ steps.package.outputs.version }}
PACKAGE_NAME: ${{ steps.package.outputs.name }}
RELEASE_URL: ${{ steps.create-release.outputs.url }}
SERVER_URL: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
- name: Comment on PR - Failure
if: |
always() &&
github.event_name == 'pull_request' &&
steps.tag-check.outputs.exists == 'false' &&
failure()
run: |
gh pr comment "$PR_NUMBER" \
--body "❌ **Release v$VERSION failed**
Please check the workflow logs for details.
🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID"
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
VERSION: ${{ steps.package.outputs.version }}
SERVER_URL: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
create-release-pr.ymlとrelease.ymlを分けている理由は次のとおりです。
create-release-pr.ymlでバージョンを間違ってもやり直しやすい(リリースPRを閉じて再度作成できる)release.ymlをリトライできる基本的にはどちらもブラウザ上で完結するので、ブラウザだけでリリースができるようになっています。
実際のリリース手順は次のとおりです。
Type: Releaseラベルを用意する


実際のサンプルは次のURLです。
OIDC連携の設定は結構面倒ですが、ここまでの設定は自動化したスクリプトを書いています。
setup-release-pr-with-oidc.tsでは次のようなことをやっています。
package.json の packageManager フィールド、なければ lockfile を見て npm / pnpm / yarn を判定するpackage.json の scripts.build の存在を確認する.github/workflows/ ディレクトリを作成するcreate-release-pr.yml をテンプレートから生成するrelease.yml をテンプレートから生成し OIDC (id-token: write) と provenance 付き publish を含めるpinact run --update で pinned された action の最新版取得を試行する(失敗時は警告表示)git status で差分を確認し workflow ファイルをコミットする(既存ならスキップ)git remote get-url origin から owner/name を抽出するgh workflow run create-release-pr を案内する一部の設定(npm 側の Trusted Publisher 設定、GitHub Actions の権限変更、初回ワークフロー実行など)は手動作業です。しかし、基本的にそのままの流れ作業で、手入力はなしでできるようにしています。
monorepoの場合はsecretlintで実装している方法を使用しています。
基本的な流れは単独パッケージと同じです。
monorepoでも1つのバージョンを使うFixed modeを使ってるので、コマンドがちょっと違うだけで流れは全く同じです。
| 操作 | 単独パッケージ | monorepo |
|---|---|---|
| バージョン更新 | npm version {patch|minor|major} |
pnpm version {patch|minor|major} --no-git-tag-version+ pnpm --recursive exec pnpm pkg set version=\"$(node -p \"JSON.parse(fs.readFileSync('package.json', 'utf8')).version\")\" |
| パッケージ公開 | npm publish --provenance |
pnpm -r publish --no-git-checks --access public --provenance |
📝 pnpm publishは内部的にnpm publishを呼び出すため、npm 11.5.1以上が必要です。lernaはまだOIDCに対応していません。(Issue #4219)
これに追加でmonorepo用のチェックjobを実行しています。
OIDC未連携のパッケージが残っている場合はコメントで通知します。

monorepoだと、後からパッケージが増えることがあるので、その度にOIDC設定をする必要があります。 そのため、未設定のパッケージを検出して通知するワークフローを追加しています。
それ以外は、単独のパッケージとmonorepoでは大きな違いはありません。
Trusted Publisherを設定してOIDC連携をすると、トークンレスでnpmパッケージをCIから公開できます。 これによって、GitHub ActionsのSecretsにnpmトークンを保存する必要がなくなります。
npmトークンの漏洩リスクがなくなり、手動でやるトークンのローテーションも不要になります。
公開後はパッケージ設定で”Require two-factor authentication and disallow tokens”を有効化しています。 この設定を有効にすると公開経路を対話操作 or OIDCでの公開のみに限定できます。
これによりパケージの公開に関して次のような制約が課されます。
そのため、汎用のnpmトークンが漏洩してもパッケージレベルでの乗っ取りを防げるようになります。
Require two-factor authentication and disallow tokens With this option, a maintainer must have two-factor authentication enabled for their account, and they must publish interactively. Maintainers will be required to enter 2FA credentials when they perform the publish. Automation tokens and granular access tokens cannot be used to publish packages.
Requiring 2FA for package publishing and settings modification | npm Docs
これは、npmアカウントをフィッシングで乗っ取られた場合も同様で、“Require two-factor authentication and disallow tokens”を有効にしてるパッケージにマルウェアを含めたバージョンを公開するのを難しくできます。
npmでMFAを設定している場合に、TOTPのようなフィッシング耐性が低いMFAだと突破されることがあります。(フィッシングサイトでTOTPまで入力させることで、アカウントを乗っ取る) しかし、“Require two-factor authentication and disallow tokens”が有効な場合は、乗っ取られたnpmアカウントで新規のMFAを追加時、npm publish時に、再びMFAの認証が求められるようになります。 これによって、攻撃者は2回MFAを突破しないといけないため、攻撃の難易度が上がります。
MFAの管理については次の記事を書いています。
Trusted Publishingを使用すると、npmは自動でprovenance attestationsを生成・公開します。 これにより、パッケージがどこでどのようにビルドされたかを示せます。 利用者は生成元とビルド経路を機械的に検証しやすくなります。
Provenanceは、次のサイトで確認できます。
現時点での主な制約は次のとおりです。
実際に運用してみて感じる不満点もあります。
npm i -g npmが必要
- Protection RulesetやEnvironmentのProtection Rulesなどを使わないと、GitHubの権限を渡すことがnpmの権限を渡すことと同義になる
これが難しくて、ベストな方法がよくわかっていません。
この変更によって、GitHub Actionsからnpmへのパッケージ公開においては、長期間有効なnpmトークンを使うよりはOIDCを使用することで確実によくなっています。 GitHub Actionsのsecretsにnpm tokenがなくなります。そのため、OIDCのtokenが漏れても短時間で無効になるため、攻撃難易度はあがります。
一方で、ローカルからnpmへpublishするのに比べて、GitHub Actionsからnpmにpublishすると若干リスクは変わる部分がある。 具体的には、従来はGitHubアカウントの乗っ取りがあっても、npmにpublishされることはありませんでした。 これは、GitHubとnpmが別々のアカウントであり、npmトークンが漏れない限りはnpmにpublishされることはなかったためです。
OIDC化によって、GitHubアカウントの乗っ取りにより、GitHub Actionsのワークフローを改変されてnpmにpublishされるリスクがあります。
具体的には次の2つのリスクが残っています。
release.ymlを直接変更されると、改変後のフローで不正なパッケージを公開される可能性がある
contents: writeとworkflow: writeがあれば可能contents: writeとPRにラベルを付与できる権限と、PRをマージできる権限があれば可能npmのTrusted PublisherのドキュメントにあるようにAdditional security measuresでこれらのリスクはもう少し軽減できる。
自分の場合は、次の2つの対策を入れているけど、もっと楽でもっと良い方法があれば教えて欲しいです。
GitHub ActionsのEnvironmentのProtection Rulesを設定することで、release.ymlが動くときにもう一段階の認証を挟んでいます。
具体的には次のような設定をしています。
npmというEnvironmentを作成npm に設定する必要がある# .github/workflows/release.yml
jobs:
publish:
runs-on: ubuntu-latest
environment: npm # 作成したEnvironment名 = npmのODC設定にもEnvironment名を設定する必要がある
steps:
# ...
この設定をすると、Actionが走る時に手動でレビューを通るような形にできます。(Required reviewersを入れなければ自動で通る)

ConfirmするとActionが実行される。

チーム内でデプロイは誰かにチェック必須にしたい場合などに、この機能が利用できます。
release.yml自体が変更されることを防ぐため、次の対策を実装している。
# CODEOWNERS
.github/CODEOWNERS @your-username
.github/workflows/ @your-username
これらの対策により、攻撃者がプロテクションルールを回避するにはPull Requestを通して変更する必要があります。 また、Code Ownersのレビューが必須になるため、攻撃者はオーナーのアカウントを乗っ取るか、Code Ownersに入っている人を乗っ取る必要があります。
Pull Request は公開で行われるため攻撃は可視化され、攻撃者にとって実行しづらくなります。
Protection RulesetのRestrict file paths(Enterprise)を使用することで、ファイルパスに対するより厳格なプロテクションルールを設定できるようになります。
これを活用すると、release.ymlに対する変更ルールをさらに厳しく設定できます。ただし、パブリックリポジトリではまだこの機能が利用できない状況です。
また、Protection RulesetやEnvironment Protection RuleがAPIで削除できるようになっています。そのため、アカウントが持っているGitHub Tokenが漏洩すると、これらのルールを削除して攻撃することも可能になってしまう。GitHub Actionsの secrets.GITHUB_TOKEN の漏洩への対策にはなっているが、アカウントのGitHub Tokenの漏洩への対策にはなっていないという問題があります。
release.ymlに関する処理を改ざんするには、必ずMFAを使わないといけないみたいな設定方法があればいいのですが、その方法が現状だとわかっていないです。
現在のOIDC化とEnvironment + Protection Ruleset(CODEOWNERS)の組み合わせにより、次の効果が得られています。
secrets.GITHUB_TOKEN)の悪用が難しくなった完全な防御は不可能ですが、攻撃コストは結構上がる状態が作られている形です。
Require two-factor authentication and disallow tokensにより、ローカルのnpmトークンが漏洩した場合の対策が可能になっています。パッケージレベルでnpmトークンのみでのpublishができなくなるためです。 そのため、影響が大きいパッケージは”Require two-factor authentication and disallow tokens”が有効になっている状態が良いです。
“Require two-factor authentication and disallow tokens” は OIDC 化とは別に設定できます。しかし monorepo では手動認証を都度挟むのは数と時間の都合で難しく、結果として未設定パッケージが多い状態でした。
Trusted Publishingを導入することで公開フローの属人性を下げつつ、長期間有効なnpm tokenなしにnpmパッケージを安全に公開できるようになります。また、Provenanceも自動生成されるため、サプライチェーンセキュリティに対する信頼性も向上します。
また、リリースフローをCI/CDにまとめることで、メンテナンスの負担軽減とセキュリティ向上を両立できます。新規パッケージや既存パッケージ、monorepoにも対応可能なため、ぜひ導入を検討してみてください。
実はこのリリースフローは3世代目です。 今まではmonorepoでしかやってなかった(npm token管理が大変なのでやれなかった)のですが、ブラウザだけで、リリースノート書けるし、Contirbutorにどのリリースで入るかが通知できて、パッケージ公開できる仕組みになっています。
OIDCを設定して、422が出たときは大体設定がミスっています
package.jsonの repository.url が、実際にpublishするworkflowが動いてるリポジトリと一致しているかを確認してくださいnpm error 422 Unprocessable Entity - PUT https://registry.npmjs.org/@honkit%2fmarkup-it - Error verifying sigstore provenance bundle: Failed to validate repository information: package.json: “repository.url” is “git+https://github.com/<publish元と異なりリポジトリ>”, expected to match “https://github.com/honkit/honkit” from provenance
setup-release-pr-with-oidc.ts - 単独パッケージ用のセットアップスクリプトJavaScript Primer v7.0.0をリリースしました 🎉
JavaScript Primer v7.0.0では、ECMAScript 2025への対応と、新しく「イテレータとジェネレータ」の章を追加しています。 Iterator Helpersをはじめとした、ES2025の新機能を学ぶことができます。
NotebookLMもES2025対応版に更新しています。
JavaScript Primer(jsprimer)では、Open Collectiveを通じてプロジェクトの更新に関わる資金を募っています。
今回のv7リリースにおいては、次の方々にご支援いただきました!
Gold Sponsors
Supporters
ご支援ありがとうございます!
JavaScript Primerスポンサーについては、次のページを参照してください。
また、ご支援いただいた資金は、jsprimerにcontributionしてくれた方へ還元できるような仕組みを引き続き運用しています。 今回から同じ金額を他のCollective(オープンソースプロジェクト)に寄付する仕組みも追加しました。 Contributorが受け取る代わりに、指定されたCollectiveへ同じ金額を寄付できます。
JavaScript Primer v7.0.0の変更点について紹介します。
リリースノートは次のページからも確認できます。
更新をメールで受け取りたい方は、次のフォームから登録できます。
Iterator.from(), .take(), .map()など)
RegExp.escape.intersection(), .union(), .difference()など)Array.prototype.flatMap(ES2019)ES2025で追加されたIterator Helpersを含む、イテレータとジェネレータに関する新しい章を追加しました。 これは、v7での最も大きな追加です。
この章では次の内容をカバーしています。
Symbol.iteratorを使ったIterableプロトコルの実装とIteratorプロトコルについてfor...of文での反復処理Iterator.from()でイテレータの作成Iterator.prototypeのメソッド
.map() - 値の変換.filter() - 値のフィルタリング.take() - 指定数の要素を取得.drop() - 指定数の要素をスキップ.flatMap() - 平坦化しながら変換.reduce() - reduce.toArray() - 配列への変換// Iterator.prototype.toArray の使用例
const iterator = Iterator.from([1, 2, 3]).map((x) => x * 2);
// Iteratorを配列に変換
const array = iterator.toArray();
console.log(array); // => [2, 4, 6]
RegExp.escapeの追加ES2025で追加されたRegExp.escapeメソッドは、文字列内の正規表現の特殊文字を自動的にエスケープします。
ユーザー入力を正規表現で安全に扱うための重要な機能です。
// RegExp.escape の例
const escaped = RegExp.escape("+");
console.log(escaped); // \+
ES2025でSetに数学的な集合演算を行うメソッドが追加されました。 これにより、複数のSet間の演算が簡潔に書けるようになりました。
// Set メソッドの例
const setA = new Set([1, 2, 3, 4, 5]);
const setB = new Set([3, 4, 5, 6, 7]);
// 和集合 (A∪B): セットAとセットBのすべての要素を含む集合
const unionSet = setA.union(setB);
console.log([...unionSet]); // => [1, 2, 3, 4, 5, 6, 7]
// 積集合 (A∩B): セットAとセットBの両方に存在する要素のみの集合
const intersectionSet = setA.intersection(setB);
console.log([...intersectionSet]); // => [3, 4, 5]
// 差集合 (A-B): セットAからセットBに含まれる要素を除いた集合
const differenceSet = setA.difference(setB);
console.log([...differenceSet]); // => [1, 2]
// 対称差集合 (A△B): セットAとセットBのどちらか一方にのみ存在する要素の集合
const symmetricDifferenceSet = setA.symmetricDifference(setB);
console.log([...symmetricDifferenceSet]); // => [1, 2, 6, 7]
// 部分集合の判定
console.log(new Set([3, 4]).isSubsetOf(setA)); // => true
// 上位集合の判定
console.log(setA.isSupersetOf(new Set([3, 4]))); // => true
// 互いに素(共通要素がない)の判定
console.log(setA.isDisjointFrom(new Set([6, 7]))); // => false
ES2025で標準化されたImport Attributesは、JSONファイルなどのJavaScript以外のファイルをインポートする際の属性を指定する構文です。
// 静的インポートの例
import jsonData from "./data.json" with { type: "json" };
console.log(jsonData.name); // => "John"
console.log(jsonData.age); // => 30
// Dynamic Import で属性を指定
const jsonData = await import("./data.json", { with: { type: "json" } });
動的にモジュールをインポートするimport()関数の説明を追加しました。
// async/await を使った Dynamic Import
async function loadModule() {
try {
const module = await import("./math-utils.js");
console.log(module.add(1, 2)); // => 3
} catch (error) {
console.error(error);
}
}
Array.prototype.flatMap (ES2019)配列の変換と平坦化を同時に行うflatMapメソッドの説明を追加しました。
const sentences = ["Hello World", "Good Morning"];
const words = sentences.flatMap(sentence => sentence.split(" "));
console.log(words); // ["Hello", "World", "Good", "Morning"]
jsprimerでは毎年更新していけるような仕組み作りの一環としてJavaScript Primer - Open Collectiveでの支援を募集しています。サイト上へのロゴの掲載やリリースノートへのロゴの掲載などの特典を含んでいます。
また、文章の修正やコード的なコミットはいつでも歓迎しています!
毎年1月頃には、次のECMAScriptのリリースに合わせた計画を立て始めています。
この時期になると次のリリースに向けてのIssueが立ち始めるので、興味がある人はリポジトリをWatchしてください!
個人的なGitHub Sponsorsも募集しています。こちらも合わせてご支援いただけると嬉しいです。
]]>macOS で Touch ID を使った「人間の確認」ができるシンプルな CLI ツール confirm-pam を作りました。
このツールを使うことで、AI Agent が任意のコマンドやスクリプトの実行する前に、Touch ID による生体認証を要求できます。 コマンドラインから実行される処理に対して、人間による明示的な確認ステップを追加する仕組みを提供します。
confirm-pam は、macOS の Touch ID を使った生体認証による確認プロンプトを提供する CLI ツールです。
基本的な使い方は次のようになります。

# 基本的な認証プロンプト
confirm-pam "この操作を実行しますか?"
# 認証成功時の終了コードは0
echo $? # 0
# 認証失敗時やキャンセル時の終了コードは1
# Touch IDの認証を失敗またはキャンセルした場合
echo $? # 1
confirm-pam はcrates.ioで公開されているため、cargo でインストールできます。
cargo install confirm-pam
また、GitHub からソースコードをクローンしてビルドできます。
git clone https://github.com/azu/confirm-pam.git
cd confirm-pam
cargo build --release
confirm-pam は非常にシンプルな API を提供しています。
confirm-pam [メッセージ]
メッセージを指定すると、Touch ID の認証ダイアログにそのメッセージが表示されます。

# 渡したメッセージが認証ダイアログに表示されます
confirm-pam "重要な変更をコミットしようとしています。続行しますか?"
終了コードによって認証結果を判定できます。例えば次のような値が返されます。
0: 認証成功1: 認証失敗またはユーザーによるキャンセル2: システムエラー(Touch ID が無効化されている等)confirm-pam は次のようなユースケースを想定して作成しています。
git commit --no-verifyの回避の確認危険な git コマンドを実行する前に確認を挟む例です。--no-verify オプションのような、通常の安全チェックを回避するコマンドの実行前に認証を要求します。
# ~/.zshrc や ~/.bashrc に追加
git() {
if [[ $@ == *'commit'* && $@ == *'--no-verify'* ]]; then
if confirm-pam "git commit --no-verifyを実行します。続行しますか?"; then
command git "$@"
else
echo "認証に失敗しました。操作をキャンセルします。人間による確認が必要です。"
return 1
fi
else
command git "$@"
fi
}
Claude Code のような AI Agent はgit commit --no-verifyで pre-commit Hooks を回避してきます。
Touch ID などの人間の認証を挟むことで、Hook を無視したコミットを防げます。

あと、人間がズルして --no-verify をつけてコミットするときに、確認の意味としてダイアログを出す方法としても使えます。
本番環境へのデプロイなど、影響範囲が大きい操作の確認ダイアログを表示する例です。
#!/bin/bash
# 危険な操作の前に確認
if confirm-pam "本番環境へのデプロイを開始します。続行しますか?"; then
echo "デプロイを開始します..."
# デプロイ処理
else
echo "デプロイがキャンセルされました"
exit 1
fi
yを入力しないと進めないようにする方法もありますが AI Agent は echo "y" | コマンド のような pipe で回避するので、人間の確認を挟みたい時に利用できます。
confirm-pam は、将来的なクロスプラットフォーム対応を見据えたレイヤー化アーキテクチャを採用しています。
main.rs (CLI エントリポイント)
↓
auth/mod.rs (認証抽象化レイヤー)
↓
platform/mod.rs (プラットフォーム固有実装)
↓
各OS実装 (macos/linux/windows)
macOS では FFI(Foreign Function Interface)を使用して、Rust から Swift コードを呼び出しています。
実装の詳細は次の通りです。
src/platform/macos/auth_helper.swift で LocalAuthentication フレームワークを使用build.rs で swiftc を使って Swift コードをコンパイルSwift 実装では、Apple の LocalAuthentication フレームワークを使用しています。
// 概念的な実装例
import LocalAuthentication
let context = LAContext()
let policy = LAPolicy.deviceOwnerAuthenticationWithBiometrics
context.evaluatePolicy(policy, localizedReason: message) { success, error in
// 認証結果の処理
}
これにより、システム標準の認証ダイアログが表示され、Touch ID で認証します。
BiometricAuthenticator トレイトによって各プラットフォームの実装を抽象化しています。
主要な API は次の通りです。
authenticate(message: &str) -> Result<bool>: 認証し結果を返すis_available() -> Result<bool>: 認証機能の利用可能性を確認コンパイル時の条件分岐で各 OS 実装を選択し、単一のバイナリとして動作します。
現在は macOS の Touch ID のみサポートしていますが、他のプラットフォームへの対応も予定しています。 しかし、自分は macOS しか使ってないので、他のプラットフォームの実装を追加したい人は PR を待ってます。
confirm-pam は、macOS で Touch ID を使った生体認証確認を簡単に追加できる CLI ツールです。 AI が回避しにくい、人間による確認を挟むためのツールとして使ってみてください。
Note: 認証をたくさん出すと無意識的に OK してしまうので、この辺をもっと工夫する必要が出てくるかもしれません。
]]>textlint v15.0.0をリリースしました!
textlint v15は、v12.3.0から非推奨としてマークされていた古いAPIをすべて削除するメジャーリリースです。
主要な変更点は次の通りです。
TextLintEngineを削除 → createLinter()TextFixEngineを削除 → createLinter()TextLintCoreを削除 → createLinter()または@textlint/kerneltextlint(シングルトンインスタンス)を削除 → createLinter()createFormatterを削除 → loadFormatter()をtextlint v15では、Node.js 20.0.0以上が必要になりました。 Nodejs 18はEOL(End of Life)となっているため、Node.js 20以上を使用する必要があります。
textlintパッケージをNode.jsモジュールとして利用する場合のみ影響を受ける変更です。
textlint v15では、非推奨となっていた次のAPIが完全に削除されました。
移行ガイドは次のページに用意しています。
TextLintEngine → createLinter()変更前(v14以前):
const { TextLintEngine } = require("textlint");
const engine = new TextLintEngine({
configFile: ".textlintrc.json"
});
const results = await engine.executeOnFiles(["*.md"]);
const output = engine.formatResults(results);
console.log(output);
変更後(v15以降):
import { createLinter, loadTextlintrc, loadLinterFormatter } from "textlint";
const descriptor = await loadTextlintrc({
configFilePath: ".textlintrc.json"
});
const linter = createLinter({ descriptor });
const results = await linter.lintFiles(["*.md"]);
const formatter = await loadLinterFormatter({ formatterName: "stylish" });
const output = formatter.format(results);
console.log(output);
TextFixEngine → createLinter()変更前(v14以前):
const { TextFixEngine } = require("textlint");
const engine = new TextFixEngine();
const results = await engine.executeOnFiles(["*.md"]);
変更後(v15以降):
import { createLinter, loadTextlintrc } from "textlint";
const descriptor = await loadTextlintrc();
const linter = createLinter({ descriptor });
const results = await linter.fixFiles(["*.md"]);
TextLintCore → createLinter()変更前(v14以前):
const { TextLintCore } = require("textlint");
const textlint = new TextLintCore();
textlint.setupRules({
"rule-name": require("./my-rule")
});
const result = await textlint.lintText("Hello world", "test.md");
変更後(v15以降):
import { createLinter } from "textlint";
import { TextlintKernelDescriptor } from "@textlint/kernel";
import { moduleInterop } from "@textlint/module-interop";
const descriptor = new TextlintKernelDescriptor({
rules: [
{
ruleId: "rule-name",
rule: moduleInterop((await import("./my-rule")).default)
}
]
});
const linter = createLinter({ descriptor });
const result = await linter.lintText("Hello world", "test.md");
textlint v15では、ESLintの動作に合わせてExit Statusが改善されました。
# v15+ behavior
textlint nonexistent-file.md # Exit Status: 2 (file search error)
textlint file-with-errors.md # Exit Status: 1 (lint errors)
textlint clean-file.md # Exit Status: 0 (success)
.textlintignoreのパターンを尊重textlint v15では、絶対パスのファイルが.textlintignoreパターンを正しく尊重しない問題が修正されました。
以前は、.textlintignoreで記載されたパターンがマッチしていても、絶対パスとして渡された場合は無視されていませんでした。
v15では、絶対パスのファイルも.textlintignoreのパターンを正しく尊重するようになりました。
# Before v15 (Bug)
textlint /absolute/path/to/ignored-file.md # Lintされてしまっていた
# v15+ (Fixed)
textlint /absolute/path/to/ignored-file.md # Lintの対象外となった
詳しくは、GitHub Issue #1412を参照してください。
textlint v14.8.0から、textlint --mcpでtextlintをMCPサーバーとして起動できます。
textlint v15では、MCPのサポートを改善しています。 2025-06-18のKey Changes - Model Context Protocolに基づいて、次の変更が行われました。
isError: trueの追加: エラーが発生した場合、isError: trueを設定することで、AIツールでエラー状態を認識できるようにしました詳しくは、次のGitHub Issueを参照してください。
textlint v15では、非推奨となっているものを削除したり、動作の一貫性を改善するような変更をしています。
またtextlintの内部的にも、pnpmとVitestへ移行し、 CIの合計時間を21m 5s → 7m 20sに短縮しています。(Windowsでnpmのインストールが遅かったのがボトルネック)
他にもMerge Gatekeeperを入れてAuto Mergeをできるようにしたり、Netlifyのキャッシュがやたら不安定なのでDeploy PR Preview actionを使ってPRのプレビューを作成するなどの改善をしています。
この辺を整理したので、textlint自体の開発体験もだいぶ良くなっています。
まだやるべきことはたくさんあるので、興味あるひとは是非Contributorとして参加してください!
実験的なものとしてtextlint-rule-preset-ai-writingとか書きながら考えていましたが、Linterの役割はちょっと広がってきているのかなと思いました。 Biome v2で追加されたAssistもそうですが、ErrorやWarningではなくSuggestionに近い部分も増えてきているように感じます。Suggestionに関してはLinterの役割なのかは微妙なところです。しかし、この辺りの機能はLinterの延長として実装されることも多くなっています。
特に自然言語はauto fixが難しいので、Suggestionのような形でHintを提供できる仕組みがあると、人間とAIにとっても使える感じのツールになるんじゃないかなーと思っています。
textlintでは、Contributionを歓迎しています。
また、GitHub Sponsorsでのサポートも歓迎しています。
機密情報を検出するSecretlintのv10.0.0をリリースしました!
このバージョンでは、デフォルトでLintの結果のシークレットをマスクして表示するように変更されました。
主要な変更点は次の通りです。詳細はリリースノートを参照してください。
最も大きな変更は、見つけたシークレットをデフォルトでマスクして表示するようになったことです。
変更前:
✖ found credential: github_token
GITHUB_TOKEN=ghp_1234567890abcdef1234567890abcdef12345678
変更後(v10.0.0):
✖ found credential: github_token
GITHUB_TOKEN=*******************************************
デバッグなどで実際のシークレット値を確認したい場合は、--no-maskSecrets オプションが使用できます。
# デフォルト(マスク表示)
$ secretlint "**/*"
~/secretlint/secretlint/examples/cli/credential
1:0 error [AWSSecretAccessKey] found AWS Secret Access Key: **************************************** @secretlint/secretlint-rule-preset-recommend > @secretlint/secretlint-rule-aws
✖ 1 problems (1 errors, 0 warnings)
# 実際のシークレット値を表示
$ secretlint --no-maskSecrets "**/*"
~/secretlint/secretlint/examples/cli/credential
1:0 error [AWSSecretAccessKey] found AWS Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYSECRETSKEY @secretlint/secretlint-rule-preset-recommend > @secretlint/secretlint-rule-aws
✖ 1 problems (1 errors, 0 warnings)
Node.js 18のサポートを終了し、Node.js 20以上が必要になりました。
Secretlint v10.0.0を使用するには、Node.js 20.0.0以上が必要です。
# Node.jsバージョンの確認
node --version # v20.0.0以上である必要があります
# 必要に応じてNode.jsをアップデート
GitHub ActionsなどのCI/CDでSecretlintを使用している場合は、Node.jsバージョンを更新してください。
- uses: actions/setup-node@v4
with:
node-version: 22
新しいデフォルトのマスク表示では、次の情報は引き続き表示されます。
実際のシークレット値のみがマスクされるため、デバッグに必要な情報は保持されています。
実際の値を確認する必要がある場合は、--no-maskSecretsオプションを使用してください。
最近の開発環境では、次のような状況が増えています。
見つかったシークレットはローテーションする必要があるため、本質的な問題は変わりません。 しかし、意図しない形でシークレットが外部に漏れるケースが増えているため、デフォルトの動作を変更しました。
Secretlint v10.0.0では、この問題を解決するためにデフォルトでマスク表示を行い、デフォルトの設定を安全側に変更しました。
Secretlint v10.0.0では、デフォルトで結果をマスク表示するように変更しました。 ツールの使われ方が変化しているため、デフォルトの設定を安全側に変更しました。
フィードバックがありましたら、GitHubのIssueでお知らせください。
]]>textlint v14.8.0をリリースしました!
今回のリリースでは、実験的な機能としてMCP(Model Context Protocol)サポートを追加しました。 MCPは、AI言語モデルが外部のツールやサービスと統一されたインターフェースを通じて連携できるプロトコルです。
textlint v14.8.0では、新しい--mcpフラグを追加しました。このフラグにより、textlintをMCPサーバーとして起動し、AI搭載のコードエディタ(VS Code with Copilot Chat、Cursor、Windsurf)から直接textlintを利用できるようになります。
Model Context Protocol(MCP)は、AI言語モデルが外部のツールやサービスと統一されたインターフェースを通じて連携するためのプロトコルです。これにより、AIアシスタントがtextlintの機能を直接利用して、文章のチェックや修正を行えるようになります。
MCPサーバーを使用するには、次の条件を満たす必要があります。
まず、プロジェクトでtextlintを設定する必要があります。 textlintにはデフォルトルールがないため、MCPサーバーを使用する前にプロジェクトで使うルールを設定する必要があります。
# textlintとルールをインストール
npm install --save-dev textlint textlint-rule-preset-ja-technical-writing
npx textlint --initを実行して、textlintの設定ファイルを作成します。これにより、基本的な設定が自動生成されます。
npx textlint --init
作成された.textlintrc.jsonファイルに必要な設定などがあれば、手動で編集します。
次の設定はtextlint --initで生成されたものです。
{
"rules": {
"preset-ja-technical-writing": true
}
}
設定が動くかは、textlintコマンドを実行して確認できます。
npx textlint README.md
これでtextlintの設定は確認できたので、あとはエディタにMCPサーバの設定を追加します。
VS CodeでtextlintのMCPサーバーを設定するには、プロジェクトに.vscode/mcp.jsonファイルを作成します:
{
"servers": {
"textlint": {
"type": "stdio",
"command": "npx",
"args": ["textlint", "--mcp"]
}
}
}
また、コマンドパレットを使用して設定することも可能です:
Ctrl+Shift+P (Windows/Linux) または Cmd+Shift+P (macOS) を押すMCP: Add Server を選択Command (stdio) を選択npx textlint --mcp を入力textlint を入力Workspace Settings を選択して .vscode/mcp.json に設定を作成プロジェクトディレクトリに .cursor/mcp.json ファイルを作成します:
{
"mcpServers": {
"textlint": {
"command": "npx",
"args": ["textlint", "--mcp"],
"env": {}
}
}
}
グローバル設定の場合は、ホームディレクトリに~/.cursor/mcp.jsonを作成し、同じ設定を記述します。
~/.codeium/windsurf/mcp_config.jsonに次の設定を追加:{
"mcpServers": {
"textlint": {
"command": "npx",
"args": ["textlint", "--mcp"],
"env": {}
}
}
}
textlint MCPサーバーでは、textlintを使った文章のチェックや修正するツールを提供しています。
AIアシスタントで使用できるプロンプトの例:
現在のファイルをtextlint MCPを使って、見つかったテキストの問題を説明してください
src/README.mdのすべてのtextlintの問題を修正してください
このMarkdownテキストの文章の問題をチェックして、改善案を提案してください
docs/ディレクトリのすべてのファイルにtextlintの自動修正を適用してください
次のような感じで、textlintのlint結果をみながら修正までやってくれます。

次のリポジトリは、VSCodeでtextlint MCPサーバーを設定したサンプルです。
git clone https://github.com/azu/textlint-mcp-example.git
cd textlint-mcp-example
npm ci
code .
textlint MCPサーバーで問題が発生した場合:
.textlintrcファイルが有効であることを確認npx textlint --mcpを実行してサーバーが起動するかテストよくある問題:
textlint v14.8.0では、実験的な機能としてMCP(Model Context Protocol)サポートを追加しました。 これにより、AI搭載のコードエディタから直接textlintの機能を利用できるようになり、エディタ上で文章チェックと修正を行えるようになります。
この機能は実験的なものですが、AI Agentとtextlintの連携が簡単になります。 ぜひお試しいただき、フィードバックをお寄せください。
textlint --mcpは、@chick-pさんが実装してくれました。
textlintでは、Contributionを歓迎しています。
また、GitHub Sponsorsでのサポートも歓迎しています。
JavaScript PrimerのNotebookLMが利用可能になりました。 NotebookLMは、Googleが提供するAIを活用したノートブック環境で、登録したソースに対して質問をしたり、情報を検索したりすることができます。
次のリンクから、JavaScript PrimerのNotebookLMにアクセスできます。
JavaScript PrimerのコンテンツがNotebookLMに入ってることで、読者は書籍の内容についてNotebookLMに直接質問したり、Podcastとして音声で聞いたりすることができるようになります。
NotebookLMを活用することで、JavaScript Primerを読み進める中で生じた疑問点をその場で解消したり、特定の構文について「このコードはどういう意味ですか?」といった質問をしてjsprimerのコンテンツをベースにして回答を得たりできます。これは、従来の書籍では難しかった、検索しにくい構文に関する疑問も解決しやすいという利点があります。
たとえば、NotebookLMでは次のような質問をしたりできます。あと、Podcastにするのがなんだかんだ便利です。
=>はどういう意味?」NotebookLM上のJavaScript Primerへは、以下のリンクからアクセスできます。
JavaScript Primerは、ECMAScript 2015以降をベースにしたJavaScriptの入門書として、常に最新の情報を提供すべく継続的なメンテナンスを行っている書籍です。 この継続的なメンテナンスの取り組みについては、TSKaigi 2025で「技術書をソフトウェア開発する」というタイトルで発表しました。
jsprimerでは、いつでもContributorsやSponsorsを募集しています!
NotebookLMをPublicに公開する機能ができたので、JavaScript PrimerのコンテンツをまとめたNotebookLMを公開しました。
NotebookLMのソースの更新は、メジャーアップデートごとに行うつもりです。 URLベースならもっと簡単に更新できる方法があるのかもしれないので、もし知っている方がいれば教えてください。
]]>TSKaigi 2025で「技術書をソフトウェア開発する」というタイトルで発表をしました。 スライドは次のページで公開しています。
基本的にはJavaScript PrimerというJavaScriptの入門書の話になっています。 どうやって作っていて、どうやって10年近くも更新を続けているのか、オープンソースとしての継続性などについて話しています。
関連:
JavaScript/TypeScript以外での通じる話が多いので、技術書や何かを伝える文章を書く人にとっても参考になるかなと思います。 今だと、LLMとかに投げる文章でも同様のことが言える場面も多いとは思います。
jsprimerの目的の一つに「変化に対応できるようにする」というものがあります。 これのためにjsprimerという書籍を更新しているので、jsprimerにContributeや金銭的に支援してくれる人や企業はいつでも募集しています。

スライド中にもありますが、現在のソフトウェア開発でオープンソースと関わらずに完結することはかなり難しいです。 そのため、入門書としての目的としてオープンソースに関わってもらうことを入れています。これはjsprimerをオープンソースで開発している理由の一つです。
オープンソースとの関わり方は色々な形があるので、jsprimerに限らずに何かしらの形で関わってみると面白いんじゃないかと思います。
GitHub Sponnsorなどでの支援も大歓迎です。
今回の発表のベースやProposalを書くときは結構LLMを使っていて、インタビュー形式でLLMに質問を考えてもらったものに回答して、そこから主張を抽出して作ってます。
スライドもLLMでアウトラインを見ながら、中身を書いていくということをやってた気がします。 昔から、アウトラインと中身を行き来して視点を変えながら書くというのをよくやっていた気がします。 アウトライナー実践入門でいうところのシェイクというやり方。
この辺は文章でもそうですが、プログラミングでも関数やモジュールという単位でアウトラインのように遠めに見ながら進めるタイミングと中身を見ながら進めるフェーズがあります。 そういった点では、文章とコードで大きな違いはないのかなと思う部分はありました。
文章やコードを読む上でも、既知から未知の方が読みやすいのは一緒で、コードの方がちょっと違うのは参照元へのジャンプ機能が充実しているという点かなと思います。 Ask the speakerでもあった気がしますが、文章を読んでる時の人間のコンテキストはかなり小さいので、jsprimerを書くときは一度に読むべきコンテキストを絞るイメージで書いています(文章ではジャンプが難しいという前提があります)。
簡単に言えば、別のページの参照を減らす(読んでるところを見てれば全部わかる)、サンプルコードを小さく保つ、パラグラフライティングで結論から書くといった話があります。 ユースケースのようなアプリケーションの章は、サンプルコードが断片的になりやすいので、それぞれのページの最後にコピペで動く完全な状態を提示することで、そこで戻って来れるような仕組みも入れています。 技術書はif文が書きにくいコード、みたいな感じで書いていくのが感覚と近いのかもしれないですね。
コードにおいては複雑度を測るのに循環的複雑度(Cyclomatic complexity)というメトリクスがありますが、 日本語だと建石評価式(自分がそう呼んでるだけ)みたいなものがあって@textstat/textstat-rule-tateish-levelとして実装したことがあります。
この辺の複雑度を低く保つことが読みやすい文章に繋がるのは同じかなと思いました。
TSKaigiお疲れ様でした!
]]>