2026-09-05

Claude Code GitHub Actionsっぽいものを作った

Table of Contents

きっかけ: 使用頻度の低いDevinがもったいない

Plain Agent というコーディングエージェントを育てています。 日中は仕事で使って、仕事が終わったらイケてない部分を改修する、というのがここ最近の趣味でした。

ただ、仕事の後だと、どう改修したら良いかというアイディアまではでるものの、目がしょぼしょぼで自力での実装は厳しく、 月額 $20 のDevinくんに任せていました。 一時期、$20プランでは足りなくて追加課金することもありましたが、機能が充実して使用頻度が減り、weekly limitを使い切れなくなってきました。

サブスク枠を無駄に使い切ろうとする貧乏精神は心穏やかじゃないし、環境にも良くないので、 固定費0の従量課金で使えるDevinくんのような開発環境を整えることにしました。

すでにある車輪:Claude Code GitHub Actions

Claude Code GitHub Actions は、GitHub ActionsのワークフローでClaude Codeを動かす仕組みで、issueやPRのコメント (@claude) をトリガーにコードレビューやコードの修正をお願いできます。 CodexやOpenCodeなど最近流行っているコーディングエージェントでも同じような仕組みが提供されています。

仕事では、安心安全安定の既製品を使えばいいですが、自分で作ることで何か得るものがあるだろうと考えて Plain Agent で実現しました。

最終的にできたワークフローはこちらです:.github/workflows/agent.yml

以下、考えたこと、必要だった改修をまとめていきます。

インターフェース

エージェンにどこまでの権限を渡すか?境界をどう守るか?

ここは既存のDockerベースのサンドボックス環境を利用することで以下のように制御しました。

json
{
  "sandbox": {
    "command": "plain-sandbox",
    "args": [
      // ...
    ],
    "separator": "--",
    "rules": [
      {
        "pattern": {
          "command": { "$regex": "^(gh|git)$" }
        },
        "mode": "sandbox",
        "additionalArgs": ["--env", "GH_TOKEN", "--allow-net", "github.com,api.github.com"]
      }
    ]
  },
  "autoApproval": {
    "defaultAction": "deny",
    "maxApprovals": 100,
    "patterns": [
      // ...
      {
        "toolName": "exec_command",
        "action": "allow"
      }
    ]
  }
}

⚠️ ただし、Dockerコンテナだけが隔離の境界となっているため、カーネルの権限昇格系の脆弱性があるとファイアウォールの設定を壊して外に通信できてしまいます。実務で使うには、もう一層外側のレイヤーを隔離する必要があると考えています。

ターン数のソフトリミット

無限に動き続けてトークンを大量消費することは避けたいので、バジェット的な終了条件が必要になります。 もともと、ツールの自動承認の設定で連続N回まではOK、という設定が実現できていました。 ただし、これは超えるとエージェントの呼び出しを止めるハードリミットに相当するもので、作業途中のブランチをpushすることもなくロストします。

  "autoApproval": {
    "maxApprovals": 100,
  } 

これを解決するために、バジェット(時間、ターン数)のソフトリミットを設定できるようにしました。 セッション時間(秒)、ターン数のソフトリミットを超えると指定したプロンプトが挿入され、一旦切り上げてくれます。

sh
plain batch -c ... --budget-soft-limit time:600s --prompt-on-budget-exceed "ちょ、ごめん。予算の都合で今の環境が止まりそうなので、一旦Pushして、どこまでやったか教えて。"
plain batch -c ... --budget-soft-limit turns:80  --prompt-on-budget-exceed ...

セッションの再開

他のエージェントと同様に、Plain Agentはユーザー入力、Agentの応答やツール呼び出しなどのイベントを .jsonl (JSON Lines) 形式で保存し、 セッションを再開することができます。 この jsonl をどこかに保存し、ワークフロー開始時に読み込むための仕組みについてです。

今回の対象はパブリックリポジトリで、特に秘匿情報はないので Artifact として保存、 /agent:33503161668 <prompt> のようにワークフローの実行IDを指定することで .jsonl をダウンロード、セッションを再開できるようにしました。

ℹ️ 当初 Actions の cache を利用しようとしましたが、今年6月頃、issue_comment のような不特定多数がトリガーしうる信頼できない イベントによって実行されたワークフローからはキャッシュが書き込めないようになっており、セッション保存用途では利用できませんでした。 (Read-only Actions cache for untrusted triggers - github.blog)

Artifactなら良いのか?と疑問に思いますが、Artifactはダウンロードの際にワークフローの実行IDを明示的に指定する必要があり、 Agentのワークフローを勝手に実行され悪意のあるプロンプトを混入されてもその実行IDを指定しない限りは問題ないと考えています。

シークレットの管理

秘匿情報はないからArtifactで良い、と書きましたが、Agentに使わせるシークレットがコンテキストに漏れないことが前提です。 sandboxの設定で、コマンドを絞ってシークレットを渡しているので一定の防御が効いてますが、もう一つ仕組みを導入しました。

このように、Agentがコマンドを実行するツールに secrets を設定することができ、ここに設定した値がコマンドの実行結果に含まれる場合は マスクしてからAgentに返す仕組みです。 生のシークレットだけでなく、base64やURLエンコードした場合でもマスクしてくれます。

json
  "tools": {
    "execCommand": {
      "secrets": {
        "GH_TOKEN": { "$env": "GH_TOKEN" }
      }
    }
  }

{ "$env" } は環境変数から設定値を読むための記法です

おわりに

個人のリポジトリで使うレベルのものとしては十分な完成度のものができたと思います。 構築の過程で考えさせられることが多く、実務で同じようなものを構築する際のヒントを得ることができました。

今後、余力ができたら以下も考えていきたいです