witr は、プロセスやポートが「なぜ動いているか」を1コマンドで答える Go 製の CLI だ(Apache-2.0、GitHub Star 21,193)。サーバーで見慣れないプロセスが動いている、ポート5000を誰かが掴んでいる——ps aux | grep で PID は分かるが、それを起動したのが systemd なのか、PM2 なのか、誰かの SSH セッションなのかは分からない。親を辿るために ps -o ppid= を何度も打ち、systemctl status を確認し、docker ps と突き合わせる。witr はこの手作業を畳む。

witr の TUI と CLI が同じ node プロセスについて systemd → PM2 → node の連鎖を表示している公式スクリーンショット
witr 公式のヒーロー画像。左が4タブ構成の TUI、右が witr node の出力で、どちらも同じ systemd → PM2 → node の連鎖を示している。下段は witr --port 5000 --short の1行回答。出典: pranshuparmar/witr

30秒でわかる witr

答える問いが違う——pslsof は「何が」動いているかを返す。witr は「なぜ」動いているかを、起点から現在までの連鎖として返す
入口は5種類——プロセス名・--pid--port--file--container のどれからでも同じ因果チェーンに合流する
判定は10段の固定順序——コンテナを最優先に、SSH → シェル → systemd → launchd → BSD rc → スーパーバイザ → cron → Windows サービス → init の順で最初に一致した1つを採用する(ソースで確認)
警告13種を自動付与——LD_PRELOADDYLD_* の設定、削除済みバイナリからの実行、危険な capability など、セキュリティ寄りの項目を含む
実測で判明した注意点——--json--env を付けなくても環境変数を全量含む。人間向け出力と --short には含まれない

本稿は v0.3.3(darwin-arm64)を実際にインストールし、ソース(vendor を除く自前コード 25,854行)を読んだうえで、README には書かれていない挙動まで含めて解説する。

witr の規模を示す統計。Star 21,193、自前コード25,854行、internal/proc が115ファイル、対応OS 4種、リリース21本
2026-08-11 時点の実測値。コード行数は vendor / docs を除いた .go ファイルの合計

witrとは——pslsofが答えない「なぜ」を埋めるCLI

witr の設計思想は README の第1章に明記されている。曰く、既存ツールは状態とメタデータを露出するが、「何が」動いているかを示すだけで、「なぜ」は利用者が複数の出力を手で突き合わせて推測するしかない。witr はその因果を明示することを唯一の目的にしている。

実際に手元の macOS で witr node を実行すると、次のような出力が返る。

Target      : Claude Helper

Process     : Claude Helper (pid 14634) {forked}
User        : user
Started     : 9 hours ago (Mon 2026-08-10 10:14:05 +00:00)

Why It Exists :
  launchd (pid 1) → Claude (pid 47166) → Claude Helper (pid 14634)

Source      : launchd

Working Dir : /

Warnings    :
  • Process is running from a suspicious working directory: /

注目すべきは Why It Exists の行だ。launchd (pid 1) → Claude (pid 47166) → Claude Helper (pid 14634) という起点から対象までの系統がそのまま1行で出る。これを ps でやるなら、PID を得て親 PID を引き、その親をまた引き……を繰り返す必要がある。

既存ツールと witr の比較。既存は何が動いているかを断片的に返し、witr は起動の因果を1出力にまとめる
witr は状態の表示ではなく、起動の因果を明示することを目的に設計されている

5つの入口はすべて PID に解決される

witr が受け付ける入口は5種類あるが、内部的にはいずれも PID へ解決されてから同じ処理に合流する。

witr nginx — プロセス名(コマンドライン全体への部分一致)
witr --pid 1234 — PID 直接指定
witr --port 5432 — そのポートを LISTEN しているプロセス
witr --file /var/lib/dpkg/lock — そのファイルを掴んでいるプロセス
witr --container redis — コンテナ名

これらは併用でき、witr nginx --pid 1234 --port 8080 のように複数の対象を1回で問い合わせることもできる。

witr が1回の実行でたどる4段階。入口を受け取る、PIDに解決する、祖先をたどる、起動源を判定して出力する
入口が違っても、PID 解決以降の処理は共通。あいまいな一致は PID 解決の段階で中断される

witrのインストールと、配布経路をどう確かめるか

witr は単一の静的バイナリとして配布され、加えて多数のディストリビューションで独立にパッケージ化されている。Repology で配布状況が一覧できる。

パッケージマネージャ経由を優先するのが公式の推奨であり(README の TIP に明記)、実運用でもこちらが素直だ。

# Debian sid / Ubuntu 26.04+ / Kali など
sudo apt install witr

# macOS / Linux
brew install witr

# conda-forge(macOS / Linux / Windows)
conda install -c conda-forge witr

# Windows
winget install -e --id PranshuParmar.witr

README には curl ... | bash のワンライナーも用意されているが、パイプ実行はダウンロードした内容を確認する余地がない。バイナリを直接取得する場合は、公式が同梱している SHA256SUMS で検証する経路を挟むほうが確実だ。

# v0.3.3 の linux-amd64 を取得して検証する例
curl -fsSLO https://github.com/pranshuparmar/witr/releases/download/v0.3.3/witr-linux-amd64
curl -fsSLO https://github.com/pranshuparmar/witr/releases/download/v0.3.3/SHA256SUMS

# 期待値と実測値を突き合わせる(一致すれば OK と表示)
grep " witr-linux-amd64$" SHA256SUMS | sha256sum -c -

# macOS の場合
shasum -a 256 -c <(grep " witr-darwin-arm64$" SHA256SUMS)

本稿の検証では darwin-arm64 のハッシュが SHA256SUMS の記載値と一致することを確認したうえで実行している。

配布物の状態について(2026-08-11 時点の実測)

本稿の執筆時点で、リポジトリの GitHub 上の説明文(description)とオーナーのプロフィール文が、いずれも暗号資産のトークンアドレスを含む文字列に書き換わっている(それぞれ 2026-08-10 19:11 UTC / 15:54 UTC 更新)。一方でコードと配布物には変更が見当たらない:直近のコミットは 2026-08-08 で維持者の署名付きコミットが並び、最新リリース v0.3.3(2026-06-24)の全アセットは作成時刻と更新時刻が一致しており、バイナリの再アップロードは発生していない。

原因は本稿では判定できないため、断定は避ける。実務上の結論はいずれの原因であっても同じで、ディストリビューションのパッケージ経由で入れるか、上記のチェックサム検証を挟むという上の手順がそのまま有効だ。

「なぜ動いているか」を決める10段の検出順序

witr の中核は internal/source/detect.goDetect() 関数にある。ここは推測ではなく上から順に判定し、最初に一致した1つを Source として採用する固定順序として実装されている。

flowchart TD A["祖先の系統を取得"] --> C{"コンテナか"} C -- "はい" --> Z1["Source: docker/podman/containerd/k8s"] C -- "いいえ" --> S{"SSH セッションか"} S -- "はい" --> Z2["Source: ssh"] S -- "いいえ" --> SH{"シェルか"} SH -- "はい" --> Z3["Source: shell/tmux/screen"] SH -- "いいえ" --> SD{"systemd か"} SD -- "はい" --> Z4["Source: systemd unit"] SD -- "いいえ" --> LD{"launchd / BSD rc か"} LD -- "はい" --> Z5["Source: launchd / rc"] LD -- "いいえ" --> SV{"スーパーバイザか"} SV -- "はい" --> Z6["Source: supervisord/PM2/s6/runit"] SV -- "いいえ" --> CR{"cron か"} CR -- "はい" --> Z7["Source: cron"] CR -- "いいえ" --> WS{"Windows サービスか"} WS -- "はい" --> Z8["Source: service"] WS -- "いいえ" --> IN{"init か"} IN -- "はい" --> Z9["Source: init"] IN -- "いいえ" --> Z10["Source: unknown"]

この順序には理由がソース内のコメントに書かれている。「プラットフォーム固有の init システムを汎用のスーパーバイザ検出より優先することで、誤検出を避ける」という意図だ。

順序が意味を持つ場面は実際にある。たとえば Docker コンテナの中で systemd が動き、その下で PM2 が Node を起動している構成では、systemd も PM2 も条件に一致しうる。witr はコンテナ判定を最優先に置いているため、この場合の Source は「コンテナ」になる。「まずどの実行環境の話をしているのか」を先に確定させる設計と言える。

スーパーバイザとして認識されるのは supervisord・PM2・s6・runit・openrc・upstart で、コンテナ側は docker・podman・containerd・kubernetes を判定対象にしている。

実測:終了コードと、名前指定で起きるあいまい一致

witr の終了コードは README 7.2 に定義がある。定義があるだけでなく実際にその通り動くかを手元で確認した。

終了コード 定義(README) 実測した状況 一致
0 発見・警告なし 定義のみ
1 発見したが警告あり --pid 14634(作業ディレクトリ警告あり)
2 見つからない 存在しないプロセス名/存在しない PID/未使用ポート
3 権限不足 定義のみ
4 引数不正・あいまいな一致 witr sleep(複数該当)/--file /etc/hosts(掴んでいるプロセスなし)
5 内部エラー 定義のみ

検証できた4項目はいずれも定義どおりだった。witr nginx --short; case $? in ... のような監視スクリプトに組み込む前提で設計されており、その前提は実際に成立している。

名前指定は「コマンドライン全体への部分一致」

ここは実測で分かった最大の落とし穴だ。witr node を実行したとき、witr が解決したのは node という名前のプロセスではなく Claude Helper だった。理由は、そのプロセスのコマンドラインに --utility-sub-type=node.mojom.NodeService という文字列が含まれていたためだ。

同様に witr sleep を実行すると、sleep コマンド本体だけでなく、コマンドラインに sleep 5 を含む zsh プロセスまで候補に挙がった。

Multiple matching processes found:

[1] sleep (pid 47656)
    sleep 300
[2] sleep (pid 47659)
    sleep 5
[3] zsh (pid 76185)
    /bin/zsh -c ... until curl ...; do sleep 5; done ...

Re-run with:
  witr --pid <pid>

つまり名前指定はプロセス名ではなくコマンドライン全体を対象とした部分一致であり、引数に紛れた文字列でも拾う。厳密に名前で照合したいときは --exact を使う。

スクリプトに組み込む際の注意:--json でも JSON が返らない場合がある

上の「複数該当」の案内は、--json を付けた場合でも JSON ではなく同じ平文で出力される(終了コードは4)。witr foo --json | jq ... のようなパイプラインは、対象があいまいになった瞬間に JSON パースで落ちる。自動化では終了コードを先に判定し、4 のときは --pid で問い合わせ直す分岐を用意しておくのが安全だ。

実務でよく使う3つの調べ方

実際の障害対応で使う形に落とすと、witr の出番はおおむね次の3パターンに収束する。

① ポートを占有しているプロセスの出どころを知る。 「8080 が塞がっていてデプロイできない」というときに、掴んでいるプロセスだけでなく、それを起動した仕組みまで一度に分かる。lsof -i :8080 で PID を得てから pssystemctl を往復する手順が1回で済む。

# ポートを掴んでいるプロセスと、その起動源を表示
witr --port 8080

# 1行だけ欲しい場合(連鎖のみ)
witr --port 8080 --short

② 消せないファイルを掴んでいるプロセスを特定する。 パッケージ管理のロックや、アンマウントできないボリュームの調査に効く。

witr --file /var/lib/dpkg/lock

③ 警告だけを拾って異常を絞り込む。 --warnings を付けると、環境変数によるライブラリインジェクションの疑いや、削除済みバイナリからの実行といった項目だけが表示される。侵害の疑いがある機器で最初に流すのに向く。

# 対象プロセスの警告のみを表示
witr nginx --warnings

# 終了コードで分岐させる(1 = 警告あり)
witr nginx --short
case $? in
  0) echo "問題なし" ;;
  1) echo "警告あり——詳細を確認" ;;
  2) echo "見つからない" ;;
  4) echo "あいまい——--pid で指定し直す" ;;
esac

--tree を付けると系統がツリー表示に、--verbose を付けるとメモリ・I/O・ファイルディスクリプタの情報が加わる。いずれも表示の粒度を変えるだけで、判定ロジックそのものは共通だ。

組み込みの警告13種と、--jsonに環境変数が含まれる点

witr は因果を表示するだけでなく、対象プロセスに対して警告を自動で付ける。detect.go から抽出すると13種類あり、その内訳は運用というよりセキュリティ寄りだ

witr がソース上で実装している13種の警告一覧。LD_PRELOAD、DYLD_、削除済みバイナリ、危険なcapability などを含む
ライブラリインジェクションを疑う項目が明示的に含まれている

とくに次の3つは、インシデント対応で最初に確認したい項目そのものだ。

LD_PRELOAD の設定(完全一致で判定)と DYLD_* の設定(前方一致で判定)——いずれも「ライブラリインジェクションの可能性」として報告される
削除済みバイナリからの実行——「ライブラリインジェクションの可能性、または更新待ち」として報告される。実行中にバイナリが差し替えられた古典的な兆候
危険な capability の保持——CAP_SYS_ADMINCAP_SYS_PTRACECAP_NET_RAWCAP_DAC_OVERRIDECAP_DAC_READ_SEARCHCAP_FOWNERCAP_SYS_MODULECAP_SYS_RAWIO の8つを危険として列挙している

「疑わしい作業ディレクトリ」の判定対象は //tmp/var/tmp の3つがハードコードされている。冒頭の実行例で警告が出たのはこれが理由で、macOS の launchd 配下のプロセスは作業ディレクトリが / になることが多いため、この警告は正常な構成でも出る点は把握しておきたい。

--json は環境変数を全量含む

これは実測しないと気づきにくい挙動なので、切り分けて確認した。

同一の PID(14634)に対して出力モードだけを変えて、環境変数が含まれるかを調べた結果が次のとおりだ。

出力モード --env の有無 環境変数の出力
既定(人間向け) なし 含まれない(該当文字列の出現0件)
--short なし 含まれない(0件)
--json なし 含まれる(16件)
--json --env あり 含まれる(16件、スキーマが別形式に変わる)
出力モードによる環境変数の扱いの違い。--json は --env なしでも環境変数を含み、既定と --short は含まない
witr v0.3.3 / macOS arm64 で実測。JSON は機械可読の全量ダンプという設計であり、脆弱性ではない

これは仕様上の欠陥ではない。--json は機械可読な全量ダンプであり、環境変数もプロセスの属性のひとつだからだ。ただし運用上の含意は無視できない。環境変数には API トークンやデータベースの認証情報が入っていることが多く、実際に上の検証でも SSH_AUTH_SOCK のパスがそのまま出力に含まれていた。

witr --port 443 --json > incident.json のようにして調査結果をチケットに添付したり、ログ基盤に送ったり、あるいは AI エージェントに読ませたりする運用は自然に発生する。そのとき環境変数ごと持ち出していることになる。共有前に jq 'del(.Process.Env)' のような除去を挟むのが実務的な対処だ。

# 環境変数を落としてから共有する
witr --port 443 --json | jq 'del(.Process.Env)' > incident.json

なお --env を付けると出力スキーマ自体が {PID, Process, Command, Env} という別の最小形に変わる。フラグによって JSON の構造が変化するため、パーサを固定で書く場合は注意がいる。

ps・lsof・systemctl・docker psとの比較——何を代替できるのか

witr がこれらを置き換えるのかというと、用途によって答えが違う。整理すると次のようになる。

ツール 主に答える問い witr が代替できるか
ps / top 何が動いているか、資源をどれだけ使っているか ✗ 常時監視・一覧用途は代替しない。単発の系統調査は代替できる
lsof / ss どのプロセスがポート・ファイルを掴んでいるか --port / --file で同等の特定はできる。汎用の FD 調査は lsof が上
systemctl status サービスの状態と unit の情報 △ 起動元の特定は代替できる。start/stop などの制御はできない
docker ps コンテナの一覧と状態 ✗ 一覧・管理は代替しない。コンテナ内プロセスの起点特定は補完する
pstree プロセスの親子関係 --tree でほぼ同等。加えて起動源(systemd/PM2 等)まで判定する

もっとも近いのは pstree だが、pstree が示すのは親子関係という構造であり、witr が加えるのは「その系統の起点が何という仕組みなのか」という意味づけだ。systemd (pid 1) → PM2 → node という連鎖を見せたうえで、Source: pm2-deploy.service (systemd) と unit ファイルのパスまで示す点が違う。

witr は制御系のコマンドを一切持たない。プロセスを kill する機能も、サービスを再起動する機能もない。調査に用途を限定した読み取り専用のツールとして設計されている。

TUI・対応プラットフォームとAI連携の現在地

引数なし、または -i で起動すると TUI ダッシュボードに入る。タブは 1. Processes / 2. Ports / 3. Containers / 4. Locks の4つで、/ キーで検索モードに切り替わる。冒頭のヒーロー画像の左側がこの画面だ。CLI と TUI は同じ判定ロジックを共有しており、どちらでも同じ因果チェーンが表示される。

4つ目の Locks タブは他のプロセス調査ツールであまり見ない構成で、ファイルロックの保持状況を一覧できる。locks_darwin.golocks_linux.golocks_windows.golocks_freebsd.go と4OS分が個別に実装されており、「ロックが外れずに処理が進まない」種類の障害を一覧から追える。TUI 側のコードは internal/tui に5,078行あり、CLI 出力の internal/output(3,128行)を上回る。表示層としては TUI のほうが厚い。

対応プラットフォームは4種だが、機能は等価ではない

対応 OS は Linux・macOS・Windows・FreeBSD の4種。この幅の広さはコードにそのまま表れていて、internal/proc は115ファイル・10,055行と自前コード全体の約4割を占める。cmdline_darwin.gocmdline_linux.gocmdline_windows.gocmdline_freebsd.go のように、プロセス情報の取得系がOSごとに個別実装されているためだ。

ただし機能が全OSで等価とは限らない。README にも互換性マトリクスがあるが、ファイル構成を見るとどこに差があるかがより具体的に分かる。

機能 実装ファイル 対応範囲
プロセス情報・コマンドライン取得 cmdline_*.go(4種) Linux / macOS / Windows / FreeBSD
ファイルロック locks_*.go(4種) 4OS すべて
ネットワーク(ポート) net_*.go 4OS すべて
ファイルディスクリプタ fd_*.go(3種) Linux / macOS / FreeBSD(Windows なし)
capability の判定 capabilities_linux.go Linux のみ
コンテナの検証 container_verify_linux.go Linux のみ

つまり「危険な capability を保持している」という警告は Linux でしか出ない。macOS で試すと因果チェーンと汎用の警告は出るが、capability 系の判定は行われない。セキュリティ用途で使うなら Linux が本命という理解でよい。実際、本稿の macOS での検証でも出た警告は作業ディレクトリに関するもののみだった。

なお、祖先を遡る処理には既知の課題がある。PID が再利用された場合に系統を誤って組み立てうる問題が維持者自身の手で Issue #219・#220 として登録されており、修正 PR #226 が提出されている(本稿執筆時点で open)。長時間稼働してPIDが一巡した環境では、この点を念頭に置いて結果を見たほうがよい。

AI 連携は「llms.txt が置かれた」段階

当サイトの読者向けに正直に書くと、witr 自体に AI 機能は無い。README にも実装にも Claude・Codex・LLM・MCP への言及は存在しない。

現状あるのは2つだけだ。

llms.txt(マージ済み)——LLM やエージェントがリポジトリの構造を把握するための要約ファイル。PR #217 で追加された
AI コーディングエージェントとの併用ドキュメント(未マージ)——Issue #223 と PR #224 で提案されている段階で、本稿執筆時点では open のまま

とはいえ、witr の設計はエージェントに渡す道具として素直ではある。--json の機械可読出力と、意味の定義された終了コードは、エージェントが「調べて、結果を判定して、次の行動を決める」形にそのまま乗る。前節で触れた環境変数の件が効いてくるのもここで、エージェントに --json をそのまま食わせる構成は、資格情報をモデルの文脈へ持ち込むことになる。併用するなら del(.Process.Env) を挟む前提で組みたい。

まとめ

witr は「何が動いているか」ではなく「なぜ動いているか」に答えることだけに用途を絞った CLI だ。読者が最初に知りたい3点をまとめると次のようになる。

何ができるか——プロセス・ポート・コンテナ・ファイルのいずれを起点にしても、それを起動した連鎖(コンテナ/systemd/PM2/シェル等)を1コマンドで出力する
何を解決するか——ps の親を手で遡り、systemctldocker ps を突き合わせる調査手順を畳む。加えて LD_PRELOAD や削除済みバイナリなど、インシデント時に確認したい兆候を自動で警告する
何を代替できるか——pstree はほぼ代替でき、lsof / systemctl の一部用途は置き換えられる。一方 ps / top / docker ps の一覧・管理用途は代替しない。制御系コマンドを持たない読み取り専用ツールである

導入は配布パッケージ経由が素直で、バイナリを直接取得する場合は同梱の SHA256SUMS で検証する。自動化に組み込むなら、終了コード4(あいまいな一致)では --json でも JSON が返らないことと、--json に環境変数が含まれることの2点を押さえておけば、実運用で困る場面はほぼない。

参照ソース

pranshuparmar/witr(公式リポジトリ・README)
internal/source/detect.go(検出順序・警告・危険 capability の実装)
README 7.2 Exit Codes(終了コードの定義)
Releases(v0.3.3 とアセット・SHA256SUMS)
Repology: witr のパッケージ配布状況
Issue #223「Using witr with AI coding agents」(提案段階・未マージ)