un tronc
Claude Code

Claude Code 定期実行の許可待ちを hook で防ぐ方法

こんにちは!アントロンのNaokoです。

自動にしたはずなのに、ときどき自分で押しに行っていませんか?

  • 毎朝できているはずのまとめが、できていない
  • 開いてみると、許可の確認が出たまま待っていた

私は毎朝7時に、Claude Code(AI に作業を頼めるアプリ)の定期実行で、その日の予定やメールをまとめた1枚を作っています。使っているのは Claude Code Desktop の「スケジュール設定されたタスク」(決めた時刻に自動でセッションを始める機能のこと)です。私の環境でも、許可の確認が出たまま63分・104分・71分止まった回がありました。

決めたコマンド以外は、確認を出さずに断る仕組みを入れました。 使ったのは hook(フック。Claude が操作をする直前などに、自分で用意したプログラムを呼び出す仕組みのこと)です。

よくある悩み:「常に許可」にしても止まる

公式の説明では、定期実行のタスクは、権限モードが Manual のとき、権限がないツールを使おうとすると、承認するまで止まります(出典:Schedule recurring tasks in Claude Code Desktop)。

公式は、タスクを作ったら Run now で1回動かし、確認が出たら「常に許可」を選んでおくよう案内しています。次の回から、同じ操作は確認なしで通ります。

ただ、「常に許可」が効くのは、一度許可したものだけです。Claude がそれまでと違うコマンドを実行しようとすると、また許可の確認が出ます。定期実行は誰も見ていない時間に動くので、確認が出ても押す人がいません。

解決の考え方:決めたもの以外は、確認を出さずに断る

  • 使うコマンドは、先に許可しておく
  • それ以外は、確認を出さずに断る。こちらを hook で行う

使うのは hook の次の項目です(出典:Hooks reference、Configure permissions)。

  • PreToolUse:Claude がツール(ファイルの読み書きやコマンドの実行など)を使う直前、許可の確認より前に動く hook
  • hook には、実行しようとしているコマンドが JSON(データを書くときの決まった形式のこと)で渡る。Bash(コマンドを実行するツールのこと)なら tool_input.command に入る
  • permissionDecision を deny にした JSON を返すと、そのツールは実行されず、理由が Claude に伝わる。何も返さなければ、ふだんどおりの判定に進む

やったこと1:見本のフォルダに設定と hook を置いた

本物とは別に見本のフォルダを作り、Claude Code に頼んで、設定ファイル .claude\settings.json、hook .claude\hooks\guard.js、許可するコマンドで動かす hello.js を置きました。パスは見本用です。

{
  "permissions": {
    "allow": ["Bash(node hello.js)"]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node \"C:/work/my-project/.claude/hooks/guard.js\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
見本のフォルダの settings.json

hook のプログラムは、渡されたコマンドが許可した1本と同じなら何も返さず、違えば deny を返します。

// PreToolUse(matcher: Bash)から呼ばれる。許可した1本以外の Bash を断る。
const ALLOWED = "node hello.js";

let raw = "";
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
  const input = JSON.parse(raw);
  const cmd = String((input.tool_input && input.tool_input.command) || "").trim();
  if (cmd === ALLOWED) return; // 何も返さない=ふだんどおり進む
  process.stdout.write(JSON.stringify({
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: `このフォルダで使える Bash は「${ALLOWED}」だけです。`,
    },
  }));
});
guard.js の中身

やったこと2:hook だけを先に試した

まず hook だけを PowerShell(Windows でコマンドを打って動かすためのソフトのこと)で動かしました。Claude Code が渡すのと同じ形の JSON を、自分で渡します。

$ok = '{"tool_name":"Bash","tool_input":{"command":"node hello.js"}}'
$ng = '{"tool_name":"Bash","tool_input":{"command":"echo skip"}}'
$ok | node .claude\hooks\guard.js
$ng | node .claude\hooks\guard.js
node hello.js では何も返らず、echo skip では deny の JSON が返っている

やったこと3:定期実行のときだけ効くようにした

hook は、定期実行かどうかに関係なく、そのプロジェクトのすべてのセッションで動きます。そのままだと、ふだんの作業のコマンドまで断ってしまいます。

見分けには、hook に渡る transcript_path(セッションの記録ファイルの場所)を使いました。私の環境では、定期実行で始まったセッションの記録の最初の発言に、<scheduled-task name="タスクの名前"> が入っていました。guard.js を写して guard-scheduled.js を作り、次の所を足しました。断る理由の文も「定期実行 morning-summary で使える…」に変えています。

const fs = require("fs");
const TASK = "morning-summary"; // 定期実行の名前

// 会話の記録から、最初のユーザー発言の文字を取り出す
function firstUserText(path) {
  for (const line of fs.readFileSync(path, "utf8").split("\n")) {
    let j;
    try { j = JSON.parse(line); } catch { continue; }
    if (j.type !== "user") continue;
    const c = j.message && j.message.content;
    if (typeof c === "string") return c;
    if (Array.isArray(c)) return c.map((x) => x.text || "").join("\n");
    return "";
  }
  return "";
}

try {
  // 定期実行の会話でなければ何も返さない
  if (!firstUserText(input.transcript_path).includes(`<scheduled-task name="${TASK}"`)) return;
  // ここから下は guard.js と同じ
} catch {
  // 記録が読めないときは何も返さない(通す)
}

見本では、定期実行のセッションの記録をまねたファイルと、ふだんのセッションをまねたファイルを作り、やったこと2の $ng に "transcript_path":"…" を足し、それぞれのファイルを入れて渡しました。

1行目は t-scheduled.jsonl の中身。定期実行のほうは deny の JSON が返り、ふだんのほうは何も返らない

本物は、ほかの人と共有しない設定ファイル .claude\settings.local.json に登録しました。

つまずきやすい所

許可するコマンドは、1文字まで同じに書く。 見本の hook は、コマンドを文字列のまま比べています。引用符の種類やパスの書き方が違うだけでも、別のコマンドとして断ります。定期実行の指示に書いたコマンドを、そのまま写すのが確実です。

hook の allow では、設定ファイルの禁止を越えられない。 allow は試していません。参考として公式で調べると、hook が allow を返しても、設定ファイルで禁止しているものは通らないとされています(出典:Configure permissions)。

結果

10月6日に本物の hook を入れ、同じ日に、朝のまとめのセッションで echo skip が確認を出さずに断られるのを確かめました。

翌朝の10月7日の回は、実行の履歴では7時13分に始まり、7時24分に「succeeded」で終わっています。セッションの記録を見ると、この回に使った Bash は、決めた2本のコマンドだけでした。

今日、定期実行の「止まった回」を確かめてみてください

自動で動かしている方は、タスクの詳細ページの Review history(実行の履歴)で、次の2つを見てみてください。

  • いつもより時間がかかった回や、終わっていない回がないか
  • その回で、何の許可の確認が出ていたか

確認が出ていたコマンドが、いつも使うものなら先に許可しておく。使う予定のないものなら、断る仕組みを考える。

Claude Code Desktop を使い始めたばかりの方は、Claude Codeデスクトップ版の始め方|毎日使ってわかった安全に使う5つのコツもあわせてどうぞ。

AI に毎日の作業を任せる仕組みづくりのご相談は、お問い合わせフォームからどうぞ。

最後まで読んでくださりありがとうございました!

コメント