Redmine×Claude Code自動化
チケットに「@claude」と書くだけでAIが動く仕組み
Redmineのチケットに「@claude ○○して」と注記を書くと、1分以内にClaude Codeがそれを拾い、無人で作業して、結果を同じチケットに注記で返してくる。私が実際に社内で動かしているこの仕組みの作り方を、コード付きで紹介します。
Redmineを Docker で立てて Claude Code とつなぐところまでは、前回の記事で紹介しています。環境構築はこちら → Claude Code × Docker Redmine 連携のすすめ
この記事はその次のステップ、「自動実行」の中身の話です。
Claude CodeとRedmineを連動すると何がいいのか
Claude Code は本来、ターミナルを開いて話しかけて使う道具です。Redmine と連動させると、依頼の入口がチケットに移ります。実際に運用して感じている違いを、連動前と連動後で並べます。
| 連動前(ターミナルで直接依頼) | 連動後(チケットに@claude) | |
|---|---|---|
| 依頼する場所 | PCの前でClaude Codeを起動して話しかける | Redmineの注記欄に書くだけ。画面を開ける端末ならPCの前でなくてもよい |
| 記録 | やり取りは会話の中に流れていく | 依頼・結果・処理時間・コストがチケットの注記として残る |
| 複数の依頼 | 1つ終わるのを待ってから次を頼む | 先に何件か書いておけば、1回の見回りにつき1件ずつ順に処理される |
| 既存の運用 | AI用の指示は別の場所に書く | 人間の担当者に振るのと同じチケットでAIに仕事を渡せる |
| 判断 | 画面を見ながらその場で確認 | 報告の注記を読んでから、人が次を決める |
- 書くだけで依頼できる:作業するのは自分のPC上の Claude Code なので、PCは起動している必要があります。ただ依頼を書く側は、Redmineの画面を開ければどこからでも構いません。
- あとから追える:「誰が、どの注記で、何を頼み、AIが何をしたか」がチケットの履歴に並びます。結果の注記には元の注記番号・所要時間・コストが自動で付きます。
- 任せきりにならない:メール送信や公開、削除、本番デプロイはしないようにAIへの指示で縛り、必要なときは「人間に委ねる」と報告させています。さらに慎重にしたいときは、実行せず計画だけを返すモードに切り替えられます。
全体の流れ
GitHub には Issue で @claude と呼ぶと Claude が動く仕組みがありますが、私が探した範囲では Redmine 向けのものは見当たりませんでした。そこで、Python の小さなスクリプトで同じことを自作しています。
Windowsタスクスケジューラ(1分ごと)
└→ redmine_runner.py(Redmine REST APIを見に行く)
└→ 「@claude」を含む未処理の注記を見つける
└→ ジョブ定義ファイル jobs/job_チケット番号_注記番号.md を作る
└→ claude -p(ヘッドレス)で実行
└→ 結果をチケットに注記で書き戻す+処理済みとして記録
常駐プロセスではなく、1分ごとに起動して、見て、終わる作りです。落ちても次の1分でまた起動するので、監視の仕組みを別に作る必要がありません。
しくみ1:注記をどうやって見つけるか
毎回、未完了のチケットを更新日時の新しい順に一定件数(私の設定では60件)取ってきて、1件ずつ注記(journals)付きで読み直します。そのうえで次の3条件をすべて満たす注記だけを実行対象にします。
- 本文に合図の文字列を含む:既定は
@claude。設定で変えられます。 - まだ処理していない:処理済みの注記IDは
state.jsonに記録しておき、二度実行しません。 - 決めた人が書いたもの:自分のユーザーIDの注記だけを拾います。他のメンバーが書いた @claude で勝手に動かないようにするためです。
def find_trigger_notes(cfg, issue_full, processed_ids):
tag = cfg["trigger_tag"] # "@claude"
out = []
for j in issue_full.get("journals", []):
notes = j.get("notes") or ""
if tag not in notes:
continue
if j["id"] in processed_ids: # 処理済みは飛ばす
continue
if cfg.get("only_from_owner", True):
if j.get("user", {}).get("id") != cfg.get("owner_user_id"):
continue # 本人以外の注記は無視
out.append(j)
out.sort(key=lambda x: x["id"]) # 古い順
return out
チケットは GET /issues.json?status_id=open&sort=updated_on:desc、注記は GET /issues/{id}.json?include=journals で取れます。見張るプロジェクトを絞りたい場合は、プロジェクトIDのリストを設定に入れておくと、それ以外のチケットは無視します。
しくみ2:Claude Code をヘッドレスで起動する
対象の注記が見つかったら、まずジョブ定義ファイルを作ります。中身はチケット番号・件名・プロジェクト名・注記の本文(@claude を取り除いたもの)・チケット本文です。
Claude Code には、このファイルのパスを伝えて「まず Read で開いて読んで」と指示します。指示本文を直接コマンド引数に詰め込まないのは、Windows の文字コード(cp932)で日本語が崩れるのを避けるためです。ファイルは UTF-8 で書き、Claude 側に読ませる二段構えにしています。
args = [claude_exe, "-p", prompt,
"--output-format", "json",
"--allowed-tools"] + tools # 事前に許可するツール
for d in [cfg["workdir"]] + cfg.get("extra_add_dirs", []):
args += ["--add-dir", d] # 触ってよいフォルダ
args += ["--permission-mode", cfg["permission_mode"]] # acceptEdits
proc = subprocess.run(args, cwd=cfg["workdir"], capture_output=True,
timeout=int(cfg.get("timeout_sec", 1800)),
creationflags=0x08000000) # 黒い窓を出さない
-p が対話なしで1回実行して終わるモードです。--output-format json にすると、最終的な回答(result)、エラーかどうか(is_error)、所要時間(duration_ms)、コスト(total_cost_usd)がまとめて返ってくるので、書き戻しに使えます。shell を介さずにリストで引数を渡しているのも、日本語を崩さないためです。
プロンプトには毎回、次のルールを埋め込んでいます。
- メール送信・SNSやWebへの公開・購入や決済・ファイルやデータの削除・本番環境へのデプロイは行わない。必要なら実行せず、その旨を結果に書いて人間に委ねる。
- 成果物は実際に作り、絶対パスで保存する。
- 最後に「実施内容の要約」と「成果物の絶対パス」をまとめて出力する(これがそのまま注記になる)。
しくみ3:結果をチケットに書き戻す
返ってきた JSON から本文を取り出し、見出しと付帯情報を付けて PUT /issues/{id}.json の notes として投稿します。成功なら「✅ Claude 実行結果」、失敗なら「⚠️ Claude 実行エラー」という見出しになるので、チケットを開けば結果がひと目でわかります。
def sanitize_bmp(text):
# 4バイト文字(多くの絵文字)を□に置き換える
return "".join(ch if ord(ch) <= 0xFFFF else "□" for ch in text)
def add_note(cfg, key, issue_id, note, extra=None):
payload = {"notes": sanitize_bmp(note)}
if extra:
payload.update(extra) # 任意でステータスや進捗率も変更
body = json.dumps({"issue": payload}, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(
"{}/issues/{}.json".format(cfg["redmine_base"], issue_id),
data=body, method="PUT",
headers={"X-Redmine-API-Key": key,
"Content-Type": "application/json"})
urllib.request.urlopen(req, timeout=30)
注記の末尾には「処理元の注記番号/日時/コスト/所要秒数」を自動で付けています。成功時にステータスや進捗率を変える設定もありますが、私は変えない設定(0)で使っています。完了にするかどうかは、報告を読んだ人が決めればよいと考えているからです。
安全のための設計
無人で動くものなので、「できること」を先に絞っています。
- 先に「処理済み」にしてから実行:実行を始める前に注記IDを
state.jsonに書き込みます。途中で落ちたり時間切れになったりしても、同じ注記で何度も走り続けることがありません。やり直したいときは、新しく @claude の注記を書けば済みます。 - 1回の見回りで1件だけ:
max_jobs_per_runを1にして、同時に何件も走らないようにしています。残りは次の見回りに回ります。 - タイムアウト:1件30分(1800秒)で打ち切ります。
- ツールと権限を絞る:
--allowed-toolsで使えるツールを列挙し、--permission-mode acceptEdits(ファイル編集は自動承認)で動かします。最初はコマンド実行(Bash)を外した「ファイルを作るだけ」の構成で始め、テストや git 操作も任せたくなってから Bash を追加しました。追加した以上、注記の内容しだいでコマンドが無人で走るので、本番への反映は人が行う運用を続けています。 - 計画だけ返すモード:設定の
modeをplanにすると、実行はせず「何を・どの順で・何を作るか」だけを注記に返します。最初に試すときや、影響の大きい依頼のときに向いています。 - ログ:見回りのたびに
runner.logに「候補何件を走査」「実行対象」「書き戻し完了(error=True/False)」を残します。ジョブ定義ファイルも残るので、何を頼んだかを後から確認できます。
注意点をひとつ。私の構成では、書き戻しに使うAPIユーザーと依頼者が同じです。AIの返答に「@claude」という文字列がそのまま入ると、理屈の上ではその注記を新しい依頼として拾いえます。気になる場合は、書き戻し専用のユーザーを作る、または返答から合図の文字列を取り除いてから投稿するのが確実です。
セットアップ手順
1. Redmine 側の準備
管理画面で REST API を有効にし、自分のユーザーの APIキーを発行します。前回の記事の Docker 環境であれば、そのまま使えます。
APIキーは、スクリプトにも設定ファイルにも書きません。実行のたびに Docker 経由で Redmine のDBから取り出します。Redmine は発行したAPIキーを tokens テーブルに action='api' として保存しているので、DBコンテナに入って1行読むだけで取れます。DBのパスワードも、コンテナの環境変数から都度読みます。これで、手元のファイルにキーやパスワードが平文で残りません。
import subprocess
def get_api_key(cfg):
c = cfg["redmine_db_container"]
# DBのパスワードはコンテナの環境変数から読む
pw = subprocess.check_output(
["docker", "exec", c, "printenv", "MYSQL_PASSWORD"]
).decode().strip()
# Redmine は APIキーを tokens テーブルに action='api' で持っている
sql = ("SELECT value FROM tokens WHERE user_id={} AND action='api' LIMIT 1;"
.format(int(cfg["api_user_id"])))
key = subprocess.check_output(
["docker", "exec", c, "mariadb", "-u" + cfg["redmine_db_user"],
"-p" + pw, cfg["redmine_db_name"], "-N", "-e", sql]
).decode().strip()
if not key:
raise RuntimeError("Redmine の APIキーを DB から取得できませんでした")
return key
MySQL のイメージを使っている場合は、mariadb を mysql に読み替えてください。
2. 設定ファイルを書く
{
"redmine_base": "http://localhost:3000",
"redmine_db_container": "redmine_db",
"redmine_db_name": "redmine",
"redmine_db_user": "redmine",
"api_user_id": YOUR_USER_ID,
"trigger_tag": "@claude",
"only_from_owner": true,
"owner_user_id": YOUR_USER_ID,
"watch_project_ids": [],
"workdir": "C:\\path\\to\\redmine_runner",
"extra_add_dirs": ["C:\\path\\to\\projects"],
"mode": "execute",
"allowed_tools": "Read,Grep,Glob,Write,Edit,WebFetch,WebSearch,TodoWrite",
"permission_mode": "acceptEdits",
"max_jobs_per_run": 1,
"timeout_sec": 1800,
"candidate_limit": 60,
"set_status_id_on_success": 0
}
watch_project_ids は空なら全プロジェクトが対象です。extra_add_dirs には、AIに触らせてよいフォルダだけを書きます。最初は mode を plan にして様子を見るのもおすすめです。
3. Claude Code の CLI にログインしておく
ここが一番つまずいた点です。タスクスケジューラから起動した Claude Code は、普段使っているアプリのログイン状態を引き継がず、「Not logged in」で止まりました。ターミナルで一度だけ CLI にログインしておく必要があります。
claude auth login claude auth status # ログインできたか確認
アプリの中で使う /login ではなく、auth login サブコマンドです。PowerShell で実行ファイルのパスを引用符付きで書く場合は、先頭に & が必要です。長く無人で回すなら claude setup-token の長期トークンや、環境変数 ANTHROPIC_API_KEY(従量課金)でも動かせます。ログインが済む前にスケジューラへ登録しないこと。毎分エラーを出し続けることになります。
4. 手動で1回動かしてから、1分ごとの実行に登録する
テスト用のチケットに「@claude 動作確認です。今日の日付を添えて一言返してください」のような注記を書き、スクリプトを手動で1回実行します。チケットに結果の注記が付けば成功です。私の最初の疎通テストでは、十数秒で結果が返ってきました。
問題がなければ、Windows のタスクスケジューラに1分ごとの実行として登録します。
schtasks /create /tn "RedmineClaudeRunner" ^ /tr "\"C:\path\to\pythonw.exe\" \"C:\path\to\redmine_runner.py\"" ^ /sc MINUTE /mo 1 /rl LIMITED /f rem 止めるとき schtasks /delete /tn "RedmineClaudeRunner" /f
python.exe ではなく pythonw.exe を使うと、毎分コンソール窓が出ません。登録・削除・手動1回実行・ログのライブ表示をそれぞれ .cmd ファイルにしておくと、ダブルクリックでオンオフできて楽です。
実際にハマったこと
- 絵文字でHTTP 500:Redmine の注記の列が3バイトの utf8 だと、🤖のような4バイト文字を含む注記を投稿した瞬間に500エラーになります。AIは絵文字を使いがちなので、投稿前に4バイト文字を置き換える処理(上の
sanitize_bmp)を入れました。✅や⚠️のような3バイトの記号は問題なく通ります。 - アプリ同梱の claude.exe が見えない:私の環境では、Claude のデスクトップアプリがストアアプリ形式(MSIX)で入っており、アプリの中から見える claude.exe のパスが、タスクスケジューラからは存在しないことになっていました。実体は
%LOCALAPPDATA%\Packages\配下にあり、そちらのパスを設定に書いて解決しました。バージョン番号のフォルダが更新のたびに増えるので、スクリプト側で最新版を探すようにしています。 - 日本語入りの .cmd が一瞬で閉じる:起動用の .cmd に日本語コメントや UTF-8 の BOM が入っていると、cmd.exe が読めずに窓が一瞬で閉じます。.cmd は英語だけ・BOMなしで書いています。
- 作業フォルダの信頼設定:ヘッドレス実行では、作業フォルダを「信頼済み」にしておかないと設定が効かないことがあるため、事前に一度信頼済みにしてあります。
- 作業フォルダを一時フォルダに置かない:最初は一時フォルダ(Temp)配下に置いていたところ、フォルダ整理の際に消えてしまいました。今はユーザーフォルダ直下の消えない場所に置いています。
どんな仕事に向いているか
実際にこの仕組みで回した依頼は、調査・比較のレポート作成、ブログ記事の下書き、画面修正の方針検討とスクリーンショット付きの確認、原稿のチェックなどです。1件あたり数分から20分ほどで結果が返ってきています。成果物はチケット番号ごとのフォルダにまとまり、注記にはその絶対パスが書かれます。
会社で使う場合に効くのは、次のような場面だと考えています。
- すでに Redmine でタスク管理している:新しいツールを覚えずに、いつものチケットからAIに依頼できます。
- 「たたき台」づくりが多い:調査メモ、文書の下書き、修正方針の検討など、人が最終判断する前段の作業に向いています。
- 依頼と結果を記録に残したい:誰がどの注記で何を頼み、AIが何を返したかがチケットに残ります。
逆に、公開・送信・本番反映のように取り返しがつかない操作は、この仕組みには任せていません。AIが用意し、人が確認して押す、という分担です。
よくある質問
Q. Redmine のプラグインを入れる必要はありますか?
A. いりません。Redmine 標準の REST API だけで動きます。Redmine 側に手を入れず、外側のスクリプトが見に行く作りです。
Q. 注記を書いてからどのくらいで動き始めますか?
A. 1分ごとに見回るので、通常は1分以内に実行が始まります。終わるまでの時間は依頼内容しだいで、私の環境では十数秒から20分ほどでした。
Q. 他のメンバーが @claude と書いても動いてしまいませんか?
A. 設定で、決めたユーザーが書いた注記だけを拾うようにしています。対象プロジェクトも絞れます。
Q. PCの電源が切れていても動きますか?
A. 動きません。Claude Code は自分のPC上で動くので、PCが起動している間だけ処理されます。書いておいた依頼は、次にPCが動いたときの見回りで拾われます。
Q. 費用はどのくらいかかりますか?
A. Claude Code の利用料だけです。結果の注記に1件ごとのコストが記録されるので、実際の金額はそこで確認できます。最初の疎通テストは十数秒で、0.1ドル強でした。
自社のRedmineやチケット管理にAIをつなぎたい方は、Posiiにご相談ください。
チケットに書くだけでAIが動く開発環境を、御社にも。
この記事の仕組みは、POSIIが毎日の開発で実際に使っているものです。チケット管理との連携、権限の線引き、運用ルールづくりまで、御社の開発環境に合わせて組み込みます。