GitHubのトレンドを眺めていたら、1位に paperclipai/paperclip がいました。説明文は「The open-source app everyone uses to manage agents at work」。その日だけで2,109スター増えていて、僕が見た時点で85,945スターです。
READMEには「If it can receive a heartbeat, it’s hired.」という一文があります。ハートビート(定期的な呼び出し)を受け取れるなら何でも社員として雇える、という意味です。AIエージェントを社員に見立てて、組織図・予算・承認フローで管理するアプリ、という触れ込みでした。
恥ずかしながら、僕はエージェントを「動かす」ことはあっても、「管理する」ことを真面目に考えたことがありませんでした。本来はClaude CodeやCodexをつないで使うものですが、APIキーを使うと課金が発生します。そこで今回はLLMを一切使わず、シェルスクリプトを1本「雇って」みることにしました。READMEの一文が本当なら、シェルスクリプトでも社員になれるはずです。
まず動かすまでに2回つまずいた
試した環境はこうです。
- 使い捨てのDockerコンテナ(Debian 12.15、メモリ4GB・CPU2コアに制限)
- Node.js v24.21.0 / npm 11.19.0
- paperclipai 2026.916.1(npmのlatest。GitHubの最新リリースもv2026.916.1)
- LLMプロバイダは未設定。エージェントはすべてprocessアダプタ(任意のコマンドを実行する方式)
最初は、いつもの癖でNode 22のコンテナを使いました。READMEの「インストールせずに試すなら npx paperclipai onboard --yes」を実行する前に、バージョンだけ確認しておこうと思ったんです。そこで最初の違和感がありました。
# Node 22 のコンテナで実行
$ node --version
v22.23.1
$ npx -y --registry https://registry.npmjs.org paperclipai --version
2026.824.1
# 終了コード 0、所要 58,054ms。npm 自体の更新案内(npm notice)以外は何も出ない
# 調べてみたところ
$ npm view paperclipai dist-tags --json
{
"latest": "2026.916.1",
"beta": "2026.921.0-beta.1",
"nightly": "2026.926.0-nightly.0",
"canary": "2026.926.0-canary.4"
}
$ npm view paperclipai@2026.824.1 engines --json # → {"node":">=20"}
$ npm view paperclipai@2026.916.1 engines --json # → {"node":">=24.11.0"}
latestの2026.916.1は engines でNode 24.11以上を要求しています。Node 22のnpxは、エラーにも警告にもせず、Node 22で動く範囲の版(2026.824.1)を入れて実行していました。npm側の版選択の仕組みまでは今回追っていませんが、結果だけ見るとそうなります。READMEにもインストーラの説明として「Node.js 24.11 or newer」と書いてあったのに、僕は読み飛ばしていました。Node 22のまま進めていたら、1か月前の版を最新だと思い込んで触っていたことになります。
本命のNode 24のコンテナでは、npm i -g paperclipai を実行しました。インストールには61,863msかかり、入ったパッケージは1,333,849,680バイト(約1.3GB)でした。中身を見ると、@openai/codex-linux-x64 が386,964,458バイト、@anthropic-ai/claude-agent-sdk-linux-x64 が215,474,149バイトあります。シェルスクリプトしか雇わない場合でも、CodexとClaudeのエージェント実行バイナリで約600MBが一緒に入ることになります。ぶっちゃけ、ここは少し重たいです。
2回目のつまずきは、コンテナの既定ユーザーであるrootのまま起動したときでした。
$ paperclipai onboard --yes # root のまま
│ Using quickstart defaults: local_trusted/private @ loopback (127.0.0.1):3100.
│ Database: embedded-postgres
│ LLM: not configured
(中略。続けて doctor が走る)
│ ✓ Background service: No usable systemd user manager was detected (common in containers and WSL1). Use paperclipai run instead.
◇ Summary ───╮
│ 12 passed │
└ All checks passed!
◇ Starting Paperclip server...
Paperclip server failed to start.
You are running this script as root. Postgres does not support running as root. If you wish to continue, configure embedded-postgres to create a Postgres user by setting the `createPostgresUser` option to true.
# 一般ユーザー(node)でやり直す
$ paperclipai onboard --yes
[12:09:13] INFO: Applying 278 pending migrations for Embedded PostgreSQL ...
[12:09:15] INFO: Server listener bound on 127.0.0.1:3100; startup recovery in progress
# onboard 開始から /api/health の 200 を確認するまで 6,813ms(2秒間隔で確認)
doctorは12項目すべて合格と言った直後に、サーバーの起動に失敗しています。doctorは「コンテナではsystemdが無いことが多い」まで気づいているのに、rootで動いていることはチェックしていないんですよね。
エラーは「createPostgresUser オプションをtrueにせよ」と言っています。ただ、これは内部で使っているembedded-postgresというライブラリのオプションです。インストールされたコードを調べてみたところ、CLIの本体(dist/index.js)で組み込みPostgreSQLを作る箇所は3か所あり、どれも databaseDir / user / password / port / persistent / initdbFlags / onLog / onError しか渡していませんでした。パッケージ全体を検索しても、embedded-postgres本体以外で createPostgresUser に触れている箇所はありません。利用者の側には、このオプションを切り替える手段が見当たらないんですよね。ライブラリはrootで動いているときに id -u postgres でOSの postgres ユーザーを探す作りなので、そのユーザーを作れば通る可能性はあります。ただし、今回は試していません。僕はおとなしく、公式のNodeイメージに最初からいる node ユーザーで起動し直しました。今度は6.8秒で起動し、278本のマイグレーションも自動で適用されました。
「ハートビートを受け取れれば雇える」の中身
コードを見ると、エージェントのつなぎ方(アダプタ)は15種類ありました。claude_local や codex_local のほかに process と http があり、process は設定した任意のコマンドを実行します。設定できるのは command / args / cwd / env / timeoutSec / graceSec です。
CLIで会社(Trial Co、課題番号の接頭辞は TRI)を作り、processアダプタのエージェントを登録しました。課題を作ってそのエージェントに割り当てると、1秒以内に実行が始まります。割り当てそのものがハートビートのきっかけになるわけです。
スクリプトには PAPERCLIP_ で始まる環境変数が24個渡されました。実行ごとのトークン(PAPERCLIP_API_KEY)や実行ID(PAPERCLIP_RUN_ID)は入っています。一方で、「どの課題のために起こされたのか」を直接示す変数は、名前を全部並べても見当たりませんでした。なので、スクリプトは自分の受信箱(inbox)を問い合わせて課題を特定する必要があります。
振る舞いを変えたスクリプトを3本用意して、1本ずつ別のエージェントとして雇いました。
- Script Bot(TRI-1担当): 環境変数を表示して、何もせずに終了する(終了コード0)
- Comment Bot(TRI-2担当): 課題にコメントを1件書いて終了する
- Closer Bot(TRI-3担当): コメント付きで、課題のステータスを
doneに更新して終了する
3本目の中身はこうです。Comment Botは最後のコマンドが paperclipai issue comment "$ID" --body ... になっているだけの同じ形で、Script Botは環境変数の名前と自分の情報(/api/agents/me)を表示するだけのスクリプトです。
#!/bin/bash
# process アダプタから実行される Closer Bot(抜粋)
A=(--api-base "$PAPERCLIP_API_URL" --api-key "$PAPERCLIP_API_KEY" --run-id "$PAPERCLIP_RUN_ID")
ID=$(paperclipai agent inbox "${A[@]}" --json \
| python3 -c 'import json,sys; d=json.load(sys.stdin); d=d if isinstance(d,list) else d.get("items",[]); print(d[0]["id"] if d else "")')
[ -n "$ID" ] && paperclipai issue update "$ID" --status done \
--comment "agent3: done at $(date -u +%T)" "${A[@]}" --json
# ↑ inbox の先頭を取っているだけなので、複数の課題が割り当たっていると別の課題を閉じかねない。
# ここは改善の余地ありで、自分の使い方次第です
結果は、終了コード0で「成功」した実行の回数も、課題の行き着く先も、3本でまったく違いました。
| エージェント | スクリプトがしたこと | 実行回数 | 起こされた理由(wakeReason) | 課題の最終状態 |
|---|---|---|---|---|
| Script Bot(TRI-1) | 環境変数を表示して終了 | 4回 | issue_assigned → missing_issue_comment → issue_continuation_needed → finish_successful_run_handoff | blocked |
| Comment Bot(TRI-2) | コメントを1件書いて終了 | 2回 | issue_assigned → finish_successful_run_handoff | blocked |
| Closer Bot(TRI-3) | コメント付きでステータスを done に更新 | 1回 | issue_assigned | done |
APIから取った実行履歴を時系列に並べると、こうなっていました。
# 全7回の実行(時刻はUTC。開始時刻 / 結果 / エージェント / 起動元 / wakeReason)
12:10:30.793 succeeded ScriptBot(TRI-1) assignment issue_assigned
12:10:31.146 succeeded ScriptBot(TRI-1) automation missing_issue_comment
12:12:07.933 succeeded CommentBot(TRI-2) assignment issue_assigned
12:12:09.219 succeeded CommentBot(TRI-2) automation finish_successful_run_handoff
12:12:45.390 succeeded ScriptBot(TRI-1) automation issue_continuation_needed
12:12:45.648 succeeded ScriptBot(TRI-1) automation finish_successful_run_handoff
12:13:01.207 succeeded CloserBot(TRI-3) assignment issue_assigned
# TRI-1 の2回目の実行に残っていたイベント
warn Run ended without an issue comment after one retry; no further comment wake will be queued
# TRI-1 に Paperclip 自身が書き込んだコメント
12:12:45 Paperclip needs a disposition before this issue can continue.
12:13:15 Paperclip could not resolve this issue's missing disposition automatically. The source assignment is unchanged and a board decision is required.
7回の実行は、どれも結果が succeeded です。それでもPaperclipは、プロセスが正常に終わったかどうかではなく、「課題にコメントが残ったか」「課題の扱い(disposition)が決まったか」を見ていました。
コメントを書かなかったScript Botは、終了の0.1秒後に missing_issue_comment という理由で1回だけ起こし直されています。2回目の実行の記録には「1回再試行してもコメントが無いので、これ以上は起こさない」という警告が残っていました。この時点では課題は in_progress のまま静かに止まっていたので、正直なところ、これで終わりだと思っていました。ところが約2分14秒後に issue_continuation_needed で再び起こされ、続けて finish_successful_run_handoff でもう1回起こされます。最後はPaperclipが「dispositionが要る」「自動では解決できないのでボード(人間)の判断が必要」とコメントし、課題は blocked になりました。なぜ2分14秒後だったのかまでは追えていません。
Comment Botはコメントの条件を満たしていました(実行記録の issueCommentStatus はどちらも satisfied)。それでもステータスを決めずに終わったので、すぐに引き継ぎ(handoff)のためにもう1回起こされました。2回目もコメントだけ書いて終わったため、結局 blocked です。1回の実行で終わったのは、コメントを書いてステータスを done にしたCloser Botだけでした。

ブラウザで開くとこう見えます。タスク一覧では、報告か完了宣言が欠けた2件に「Recovery needed」のバッジが付いていて、Inboxにも3件溜まっていました。管理する人間の側から見ると、「エージェントが黙って終わった仕事」がきちんと目立つ場所に上がってくる作りです。

TRI-2を開くと、Comment Botの2件のコメントの間に「Missing issue disposition」、最後に「Missing disposition recovery blocked」が挟まっていて、何が起きたのかを時系列で追えます。画面の時刻はブラウザの時刻(日本時間)なので、UTCで書いた上のログとは9時間ずれて見えます。
ついでにコストも見ておきました。Script Botの実行記録は usageJson も costUsd も空(null)で、今月の使用額(spentMonthlyCents)は0のままでした。予算の上限はトークンの使用量を報告してくるアダプタを前提にしているのだろう、というのは僕の推測で、確かめてはいません。
試してみての所感
今回学んだことを並べておきます。
- Node 22で
npx paperclipaiを実行すると、エラーにならずに1か月前の2026.824.1が入る。最新版はNode 24.11以上が必要 - rootで起動すると、doctorが12項目すべて合格でもサーバーが起動しない。一般ユーザーなら6.8秒で起動した
- processアダプタのエージェントには、担当する課題のIDが環境変数では渡されない。自分でinboxを問い合わせる
- 終了コード0は仕事の完了を意味しない。コメントとステータス更新(disposition)の両方を返して、初めて1回の実行で終わる
- それを返さないと、Paperclipが自動で起こし直し、最後は人間の判断待ちの
blockedになる
先輩によく言われる「終了コード0は、プロセスが終わったという以上の意味は無いからね」という言葉を思い出しました。Paperclipは、まさにその前提で作られている印象です。エージェントを社員に見立てるのは比喩だと思っていましたが、報告も完了宣言もしない社員は上司に何度も呼び戻され、最後は案件ごとボードに上げられる。そこまで作り込まれていたのは、ちょっと面白かったりします。
一方で、今回見たのはLLMを使わない一番素朴なアダプタだけです。Claude CodeやCodexのアダプタには、この作法をエージェントに教える仕組みがあるのだと思いますが、今回は未確認です。約1.3GBのインストールサイズも含めて、自分の手元に常駐させるかどうかは様子見にします。次は無料で使えるローカルLLMをつないで、本物のエージェントがコメントとステータス更新をきちんと返すのかを見てみたいです。