Claude Code×Slack・Notion API連携【2026】社内の情報共有とナレッジ整理をAIエージェントで自動化
「Slackで流れていった大事な話が、どこにも残っていない」「Notionのタスク表、期限切れが放置されている」——情報共有の困りごとの多くは、SlackとNotionの“あいだ”で起きています。この記事では、Claude Code(ターミナルで動くAnthropicのコーディングエージェント)にSlack API・Notion APIをつなぎ、要約・リマインド・ToDo起票・FAQ下書きといった社内の情報整理を任せる構成を、動くコードと設定ファイル付きで解説します。キーワードは「読むのは自動、書くのは人が確認してから」です。
- SlackはBot Token(xoxb-)+必要最小限のスコープ、Notionは内部インテグレーション+対象ページだけ共有。AIに見せる範囲を「トークンの権限」で先に絞る
- APIを叩くのはレビュー済みのPythonスクリプト(
slack_sdk・notion-client)。Claude Codeは読む・要約する・下書きを作る係にする - スクリプトを
scripts/read/とscripts/write/に分け、permissionsで読み取りはallow、書き込みはaskにする。投稿・起票は人が内容を見てから - DMは対象外、個人情報・社外秘は書き出さない、レート制限を守る。定型処理はSkills化し、ヘッドレス実行は「下書きまで」に留める
何ができるか:社内の情報共有で効く4つのユースケース
SlackとNotionは、多くの会社で「流れる情報(フロー)」と「残す情報(ストック)」の役割を分担しています。ところが、フローからストックへ移す作業——要約する、転記する、ToDoに起こす——は人手に頼りがちで、忙しい週ほど抜け落ちます。ここがAIエージェントの出番です。
| ユースケース | 読む(入力) | 書く(出力) | 人の確認ポイント |
|---|---|---|---|
| ① 週報の自動要約 | Slackの特定チャンネルの1週間分 | Notionの週報DBに1ページ登録 | 要約の誤読・言い過ぎ、載せてはいけない話題がないか |
| ② 期限切れタスクのリマインド | NotionのタスクDB(期限<今日 かつ 未完了) | Slackのチームチャンネルに担当者メンション付きで投稿 | 担当者の取り違え、すでに終わっているのにステータス未更新のもの |
| ③ 議事録からToDo起票 | Notionの議事録ページ | NotionのタスクDBにToDoを起票 | 拾い漏れ・拾いすぎ、担当と期限の妥当性 |
| ④ FAQページの下書き | Slackの社内FAQチャンネルの質問と回答 | NotionにFAQの下書きページ | 回答の正しさ、古いルールが混ざっていないか |
4つとも、構造は同じです。APIでデータを取ってくる → Claudeが読んで要約・抽出する → 人が確認する → APIで書き込む。この「人が確認する」をどこに・どう挟むかが、設計の中心になります。
「Claude CodeにSlackとNotionのAPIを直接叩かせればいいのでは?」と思うかもしれません。対話で試すぶんにはそれでも動きますが、業務として毎週まわすなら、APIを叩く部分はレビュー済みのスクリプトに固定することをおすすめします。理由は、①どのチャンネル・どのDBに触るかをコードで制限できる、②書き込みの形式(プロパティ名、ブロック構造)が毎回ぶれない、③トークンを使う処理がスクリプトの中に閉じるので、Claude自身がトークンを読む必要がない——の3点です。


準備と認証:Slackアプリ(Bot Token・スコープ)とNotionインテグレーション
Slack:アプリを作ってBot Tokenを発行する
- Slackの開発者向けページ(api.slack.com)でアプリを新規作成し、自社のワークスペースを選ぶ
- 「OAuth & Permissions」でBot Token Scopesを追加する(下表。必要なものだけ)
- ワークスペースにインストールし、Bot User OAuth Token(
xoxb-で始まる)を控える - 要約したいチャンネル・投稿したいチャンネルで、アプリをチャンネルに招待する(
/invite @アプリ名)。招待していない公開チャンネルの履歴は、原則として読めません
| スコープ | 用途 | この記事での扱い |
|---|---|---|
channels:read | 公開チャンネルの情報取得 | 付ける(チャンネル種別の確認) |
channels:history | 公開チャンネルのメッセージ取得 | 付ける(週報・FAQの材料) |
chat:write | メッセージ投稿 | 付ける(リマインド投稿。write系スクリプトだけが使う) |
users:read | ユーザー名の解決 | 付ける(要約に表示名を出すため) |
users:read.email | メールアドレスからユーザーを引く | 付ける(NotionとSlackの担当者を突き合わせるため) |
groups:history | プライベートチャンネルの履歴 | 付けない(必要になったら参加者の合意を取ってから) |
im:history / mpim:history | DM・グループDMの履歴 | 付けない(DMは対象外) |
スコープの名称と、必要なスコープの組み合わせは執筆時点のものです。メソッドごとに必要なスコープは公式リファレンスに書かれているので、スクリプトで使うメソッドから逆算して付けてください。「とりあえず全部」は厳禁です。DMの履歴スコープを付けないことで、コードのバグやプロンプトの指示ミスがあってもDMは技術的に読めない状態にできます。
Notion:内部インテグレーションを作り、ページを共有する
- Notionの開発者向け画面で内部インテグレーション(Internal integration)を作成し、トークンを控える
- 機能(Capabilities)で、コンテンツの読み取り・挿入・更新のうち必要なものを選ぶ。担当者のメールで突き合わせるなら、ユーザー情報(メールアドレスを含む)の読み取りも許可する
- 対象のデータベースやページを開き、右上のメニューから「接続」でインテグレーションを追加する。これをしないとAPIからは「存在しない」扱いになる
- ページURLの末尾にある32桁の英数字が、ページID/データベースIDになる
Notionの内部インテグレーションは、ワークスペース全体ではなく、接続したページとその配下だけが見える仕組みです。これは不便に見えて、実は大きな安全装置です。週報DB・タスクDB・議事録の親ページ・FAQの親ページだけを接続し、人事や経営会議のページは接続しない——これだけで、AIが触れる範囲をワークスペースの管理画面で説明できる形にできます。
Notion-Version でバージョンを指定します。2025年のバージョン(2025-09-03)でデータベースと「データソース」が分離され、DBの検索(query)や pages.create の親指定の書き方が変わりました。この記事のコードは広く使われている従来形式(2022-06-28、database_id指定)で書き、バージョンをコード内で固定しています。新規に作る場合は、最新のバージョンとnotion-clientの対応状況を公式ドキュメントで確認し、Claude Codeに「最新版の書き方に移行して」と頼むのが確実です。
最後にライブラリを入れ、トークンは.envに置きます(.envは.gitignoreに追加)。
python -m venv .venv && source .venv/bin/activate
pip install slack_sdk notion-client python-dotenvSLACK_BOT_TOKEN=xoxb-...
# 読んでよいチャンネルIDの許可リスト(カンマ区切り)
SLACK_ALLOWED_CHANNELS=C0123GENERAL,C0456FAQ
SLACK_REMIND_CHANNEL=C0789TEAM
NOTION_TOKEN=ntn_xxxxxxxxxxxx
NOTION_WEEKLY_DB=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NOTION_TASK_DB=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NOTION_FAQ_PARENT=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxプロジェクト構成:read と write を分ける
この記事の設計でいちばん大事なのが、読み取り専用のスクリプトと、書き込みをするスクリプトを別ディレクトリに分けることです。こうすると、後述のpermissionsで「read系は確認なしで実行してよい」「write系は毎回確認」と、ディレクトリ単位で扱いを変えられます。
office-agent/
├── CLAUDE.md # Claude Code が毎回読む手順書
├── .claude/
│ ├── settings.json # permissions(allow / ask / deny)
│ └── skills/
│ └── weekly-digest/SKILL.md
├── .env # トークン類(deny+.gitignore)
├── scripts/
│ ├── common.py # クライアント生成・許可リストの確認
│ ├── read/
│ │ ├── slack_fetch.py # チャンネル履歴 → data/*.json
│ │ ├── notion_overdue.py # 期限切れタスク → data/overdue.json
│ │ └── notion_page_text.py# 議事録ページ → テキスト
│ └── write/
│ ├── notion_publish_md.py # drafts/*.md → Notion(週報DB / FAQ)
│ ├── notion_create_tasks.py# drafts/todos.json → タスクDB
│ └── slack_remind.py # data/overdue.json → Slack投稿
├── data/ # 取得した生データ(.gitignore)
└── drafts/ # Claude が書く下書き(人が確認する)write系スクリプトには、さらに「既定はドライラン(何を書き込むか表示するだけ)、--commitを付けたときだけ本当に書き込む」という作りを入れます。permissionsの確認と合わせて、書き込みには二段階のブレーキがかかります。
スクリプト実例:slack_sdk と notion-client
共通部分:クライアントと許可リスト
"""Slack / Notion クライアントの生成と、触ってよい範囲のチェックをまとめる"""
import os
import sys
from datetime import timedelta, timezone
from dotenv import load_dotenv
from notion_client import Client
from slack_sdk import WebClient
from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
load_dotenv()
JST = timezone(timedelta(hours=9))
# この記事のコードは従来形式(database_id 指定)で書いているため、バージョンを固定する
NOTION_VERSION = "2022-06-28"
def slack() -> WebClient:
client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
# 429(レート制限)が返ったら Retry-After に従って自動で待って再試行する
client.retry_handlers.append(RateLimitErrorRetryHandler(max_retry_count=3))
return client
def notion() -> Client:
return Client(auth=os.environ["NOTION_TOKEN"], notion_version=NOTION_VERSION)
def allowed_channels() -> set:
return {c.strip() for c in os.environ.get("SLACK_ALLOWED_CHANNELS", "").split(",") if c.strip()}
def ensure_channel_allowed(client: WebClient, channel_id: str) -> dict:
"""許可リスト外・DM・プライベートチャンネルなら処理を止める"""
if channel_id not in allowed_channels():
sys.exit(f"{channel_id} は SLACK_ALLOWED_CHANNELS にありません")
info = client.conversations_info(channel=channel_id)["channel"]
if info.get("is_im") or info.get("is_mpim") or info.get("is_private"):
sys.exit("DM・グループDM・プライベートチャンネルは対象外です")
return info許可リストは「スコープで読めない」ことに加えた二重目のガードです。ボットを招待したチャンネルが増えても、SLACK_ALLOWED_CHANNELSに入れたものしか読みません。
Slack:1週間分の履歴を取得する(read)
"""使い方: python scripts/read/slack_fetch.py C0123GENERAL 7
指定チャンネルの直近N日分(スレッド返信を含む)を data/ に JSON で保存する。読み取り専用。"""
import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from common import JST, ensure_channel_allowed, slack # noqa: E402
client = slack()
_names: dict = {}
def display_name(user_id: str) -> str:
if not user_id:
return "(unknown)"
if user_id not in _names:
profile = client.users_info(user=user_id)["user"]["profile"]
_names[user_id] = profile.get("display_name") or profile.get("real_name") or user_id
return _names[user_id]
def to_item(m: dict) -> dict:
ts = datetime.fromtimestamp(float(m["ts"]), JST)
return {"time": ts.strftime("%m/%d %H:%M"), "user": display_name(m.get("user", "")), "text": m.get("text", "")}
def fetch(channel_id: str, days: int) -> list:
ensure_channel_allowed(client, channel_id)
oldest = (datetime.now(JST) - timedelta(days=days)).timestamp()
items, cursor = [], None
while True:
resp = client.conversations_history(channel=channel_id, oldest=str(oldest), limit=200, cursor=cursor)
for m in resp["messages"]:
if m.get("subtype"): # 入退室・ボット投稿などは除外
continue
item = to_item(m)
if m.get("reply_count"):
thread = client.conversations_replies(channel=channel_id, ts=m["ts"], limit=200)
item["replies"] = [to_item(r) for r in thread["messages"][1:]]
items.append(item)
cursor = resp.get("response_metadata", {}).get("next_cursor")
if not cursor:
break
return list(reversed(items)) # 新しい順で返るので古い順に並べ直す
if __name__ == "__main__":
channel, days = sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 7
data = fetch(channel, days)
out = Path("data") / f"slack_{channel}_{datetime.now(JST):%Y%m%d}.json"
out.parent.mkdir(exist_ok=True)
out.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"{len(data)} messages -> {out}")ポイントは3つです。ページング(response_metadata.next_cursorが空になるまで繰り返す)、スレッド返信の取得(conversations_historyは親メッセージしか返さないので、reply_countがあるものだけconversations_repliesで取りに行く)、そしてユーザーIDを表示名に変換してからClaudeに渡すことです。IDのままだと、要約で「U04XXXXさんが…」となり、読み手に伝わりません。
Notion:期限切れタスクを抽出する(read)
タスクDBには「名前(タイトル)」「期限(日付)」「ステータス(ステータス)」「担当(ユーザー)」のプロパティがある前提です。プロパティ名は各社のDBに合わせて変えてください。
"""使い方: python scripts/read/notion_overdue.py
タスクDBから「期限が今日より前 かつ ステータスが完了以外」を抽出し data/overdue.json に保存する。"""
import json
import os
import sys
from datetime import datetime
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from common import JST, notion # noqa: E402
client = notion()
today = datetime.now(JST).date().isoformat()
query_filter = {
"and": [
{"property": "期限", "date": {"before": today}},
{"property": "ステータス", "status": {"does_not_equal": "完了"}},
]
}
def plain(rich: list) -> str:
return "".join(t.get("plain_text", "") for t in rich)
tasks, cursor = [], None
while True:
params = {"database_id": os.environ["NOTION_TASK_DB"], "filter": query_filter, "page_size": 100}
if cursor:
params["start_cursor"] = cursor
# 従来形式のDBクエリ。新しいAPIバージョンでは data source 側の query に移っている(公式で確認)
resp = client.databases.query(**params)
for page in resp["results"]:
props = page["properties"]
tasks.append({
"title": plain(props["名前"]["title"]),
"due": props["期限"]["date"]["start"],
"assignee_emails": [p.get("person", {}).get("email") for p in props["担当"]["people"]],
"url": page["url"],
})
if not resp["has_more"]:
break
cursor = resp["next_cursor"]
Path("data").mkdir(exist_ok=True)
Path("data/overdue.json").write_text(json.dumps(tasks, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"{len(tasks)} overdue tasks -> data/overdue.json")「期限切れかどうか」の判定をClaudeにさせず、APIのフィルタで決定的に行っているのがポイントです。LLMに日付比較をさせると、タイムゾーンや「今日」の解釈でまれにずれます。判定はコード、文面の調整や「このタスクは実は終わっていそう(関連スレッドで完了報告がある)」といった気づきはClaude、と分けます。
Notion:議事録ページのテキストを取り出す(read)
"""使い方: python scripts/read/notion_page_text.py <page_id>
ページ本文をブロック単位でたどり、見出し・箇条書き・チェックボックスを含むテキストとして出力する。"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from common import notion # noqa: E402
client = notion()
PREFIX = {"heading_1": "# ", "heading_2": "## ", "heading_3": "### ", "bulleted_list_item": "- ", "numbered_list_item": "1. "}
def walk(block_id: str, depth: int = 0) -> None:
cursor = None
while True:
params = {"block_id": block_id, "page_size": 100}
if cursor:
params["start_cursor"] = cursor
resp = client.blocks.children.list(**params)
for b in resp["results"]:
body = b.get(b["type"], {})
text = "".join(t["plain_text"] for t in body.get("rich_text", []))
if b["type"] == "to_do":
prefix = "[x] " if body.get("checked") else "[ ] "
else:
prefix = PREFIX.get(b["type"], "")
if text:
print(" " * depth + prefix + text)
if b.get("has_children"):
walk(b["id"], depth + 1)
if not resp["has_more"]:
break
cursor = resp["next_cursor"]
if __name__ == "__main__":
walk(sys.argv[1])Notionのページ本文は「ブロック」の木構造で、1回のリクエストで返るのは直下の子ブロックだけです。has_childrenを見て再帰的にたどらないと、トグルの中に書いたToDoを拾い漏らします。
Notion:下書きMarkdownを週報DB・FAQページに登録する(write)
"""使い方:
python scripts/write/notion_publish_md.py weekly drafts/weekly_20260926.md # ドライラン
python scripts/write/notion_publish_md.py weekly drafts/weekly_20260926.md --commit # 登録
python scripts/write/notion_publish_md.py faq drafts/faq_20260926.md --commit
下書きの1行目(# で始まる行)をタイトルにし、残りをブロックに変換して登録する。"""
import argparse
import json
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from common import JST, notion # noqa: E402
MAX_BLOCKS = 100 # 1リクエストで渡せる子ブロック数の上限
MAX_TEXT = 2000 # 1つのテキスト要素の文字数上限
def to_blocks(lines: list) -> list:
blocks = []
for line in (ln.rstrip() for ln in lines):
if not line:
continue
if line.startswith("## "):
kind, text = "heading_2", line[3:]
elif line.startswith("- "):
kind, text = "bulleted_list_item", line[2:]
else:
kind, text = "paragraph", line
blocks.append({"object": "block", "type": kind,
kind: {"rich_text": [{"type": "text", "text": {"content": text[:MAX_TEXT]}}]}})
if len(blocks) > MAX_BLOCKS:
sys.exit(f"ブロック数が {len(blocks)} あります。下書きを短くしてください(上限 {MAX_BLOCKS})")
return blocks
def build_request(target: str, title: str, blocks: list) -> dict:
title_value = {"title": [{"text": {"content": title}}]}
if target == "weekly":
today = datetime.now(JST).date()
return {
"parent": {"database_id": os.environ["NOTION_WEEKLY_DB"]},
"properties": {
"名前": title_value,
"対象期間": {"date": {"start": (today - timedelta(days=6)).isoformat(), "end": today.isoformat()}},
"状態": {"select": {"name": "要確認"}},
},
"children": blocks,
}
# FAQ はページの子ページとして作る(親がページのときのプロパティは title だけ)
return {"parent": {"page_id": os.environ["NOTION_FAQ_PARENT"]}, "properties": {"title": title_value}, "children": blocks}
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("target", choices=["weekly", "faq"])
ap.add_argument("draft")
ap.add_argument("--commit", action="store_true", help="付けたときだけ実際に登録する")
args = ap.parse_args()
draft = Path(args.draft)
if draft.parent.name != "drafts":
sys.exit("drafts/ 配下のファイルだけ登録できます")
lines = draft.read_text(encoding="utf-8").splitlines()
title = lines[0].lstrip("# ").strip()
body = build_request(args.target, title, to_blocks(lines[1:]))
if not args.commit:
print(json.dumps(body, ensure_ascii=False, indent=1)[:3000])
print("\n(dry-run) 内容を確認し、問題なければ --commit を付けて再実行してください")
sys.exit(0)
page = notion().pages.create(**body)
print("created:", page["url"])週報DBには「状態=要確認」で登録しています。登録後にもう一段、Notion上でチームリーダーが目を通して「公開」に変える運用にすると、DBのビューで「要確認のものだけ」を一覧でき、確認漏れが見える化されます。上限値(1リクエストの子ブロック100件、テキスト2,000文字)は執筆時点のもので、超えるとAPIがエラーを返します。
Notion:ToDoを起票する(write)
議事録から抽出したToDoは、Claudeにdrafts/todos.jsonとして書かせ、人が確認・修正してから起票します。JSONにするのは、起票前に機械的なチェック(件数、必須項目、日付形式)をかけられるからです。
"""使い方: python scripts/write/notion_create_tasks.py drafts/todos.json [--commit]
todos.json の形式: [{"title": "...", "due": "2026-10-03", "owner": "山田", "source_url": "https://..."}]"""
import argparse
import json
import os
import sys
from datetime import date
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from common import notion # noqa: E402
MAX_TASKS = 15
def validate(todos: list) -> None:
if not isinstance(todos, list) or not todos:
sys.exit("ToDoが空です")
if len(todos) > MAX_TASKS:
sys.exit(f"{len(todos)}件は多すぎます(上限{MAX_TASKS}件)。議事録を分けてください")
for i, t in enumerate(todos):
if not t.get("title") or len(t["title"]) > 80:
sys.exit(f"{i}件目: title が空か長すぎます")
if t.get("due"):
date.fromisoformat(t["due"]) # 形式が不正なら例外で止まる
def to_properties(t: dict) -> dict:
props = {
"名前": {"title": [{"text": {"content": t["title"]}}]},
"ステータス": {"status": {"name": "未着手"}},
# 担当は people 型に入れるにはユーザーIDが要るため、まずは仮の名前をテキストで入れて人が付け替える
"担当(仮)": {"rich_text": [{"text": {"content": t.get("owner", "")}}]},
"出典": {"url": t.get("source_url") or None},
}
if t.get("due"):
props["期限"] = {"date": {"start": t["due"]}}
return props
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("path")
ap.add_argument("--commit", action="store_true")
args = ap.parse_args()
todos = json.loads(Path(args.path).read_text(encoding="utf-8"))
validate(todos)
for t in todos:
print(f"- {t['title']} / 期限 {t.get('due') or '未設定'} / 担当 {t.get('owner') or '未定'}")
if not args.commit:
print("\n(dry-run) 上記を起票します。問題なければ --commit を付けて再実行してください")
sys.exit(0)
client = notion()
for t in todos:
client.pages.create(parent={"database_id": os.environ["NOTION_TASK_DB"]}, properties=to_properties(t))
print(f"{len(todos)} tasks created")Slack:期限切れタスクをリマインドする(write)
リマインドはDMではなくチームのチャンネルに、担当者をメンションして投稿する設計にしています。DMで個別に催促するより、誰に何が届いたかがチームから見え、ボットの挙動を監視しやすいからです(催促を公開したくない文化なら、DMを選ぶ判断もあります。その場合も送信は人の確認後に)。
"""使い方: python scripts/write/slack_remind.py [--commit]
data/overdue.json を担当者ごとにまとめ、SLACK_REMIND_CHANNEL にメンション付きで1通だけ投稿する。"""
import argparse
import json
import os
import sys
from collections import defaultdict
from pathlib import Path
from slack_sdk.errors import SlackApiError
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from common import slack # noqa: E402
client = slack()
def mention(email: str) -> str:
if not email:
return "担当未設定"
try:
uid = client.users_lookupByEmail(email=email)["user"]["id"]
return f"<@{uid}>"
except SlackApiError:
return email # 見つからなければメンションせず文字で出す
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--commit", action="store_true")
args = ap.parse_args()
tasks = json.loads(Path("data/overdue.json").read_text(encoding="utf-8"))
if not tasks:
sys.exit("期限切れタスクはありません")
by_owner = defaultdict(list)
for t in tasks:
for email in (t["assignee_emails"] or [""]):
by_owner[email].append(t)
lines = [":spiral_calendar_pad: 期限を過ぎているタスクがあります(Notionのステータス更新もお願いします)"]
for email, items in by_owner.items():
lines.append(f"\n{mention(email)}")
lines += [f"• <{t['url']}|{t['title']}>(期限 {t['due']})" for t in items]
text = "\n".join(lines)
print(text)
if not args.commit:
print("\n(dry-run) 問題なければ --commit を付けて再実行してください")
sys.exit(0)
client.chat_postMessage(channel=os.environ["SLACK_REMIND_CHANNEL"], text=text)
print("posted")Slackのメッセージでは、<@ユーザーID>でメンション、<URL|表示テキスト>でリンクになります。投稿本文をテンプレートでスクリプトが組み立てるのは、担当者やURLといった「間違えてはいけない部分」をLLMの生成に通さないためです。
CLAUDE.md の例
Claude Codeはプロジェクト直下のCLAUDE.mdを起動のたびに読み込みます(/initで雛形を作れます)。ここに「どのスクリプトで何を取るか」「下書きの書き方」「やってはいけないこと」を書いておくと、担当者が変わっても同じ品質で回ります。
# CLAUDE.md(社内情報共有エージェント)
## 役割
- Slack / Notion のデータを読んで、要約・ToDo抽出・FAQ下書きを drafts/ に作る
- Slack への投稿・Notion への登録は、必ず人が drafts/ を確認したあとに write 系スクリプトで行う
## データの取り方(APIを直接叩かない。必ずスクリプト経由)
- Slack履歴: `python scripts/read/slack_fetch.py <channel_id> 7` → data/slack_*.json
- 期限切れタスク: `python scripts/read/notion_overdue.py` → data/overdue.json
- 議事録本文: `python scripts/read/notion_page_text.py <page_id>`
- 対象チャンネルは .env の許可リストにあるものだけ。DM・プライベートチャンネルは扱わない
## 下書きのルール
- 週報: drafts/weekly_YYYYMMDD.md。1行目は「# 週報 MM/DD〜MM/DD #チャンネル名」
- 「## 決まったこと」「## 進行中」「## 困りごと・相談」「## 来週の予定」の4見出し
- 各項目の末尾に元メッセージの日時(例: 09/24 14:05)を付け、根拠をたどれるようにする
- 推測で補わない。決定か検討中か曖昧なものは「検討中」に入れる
- ToDo: drafts/todos.json(形式は scripts/write/notion_create_tasks.py の docstring 参照)
- 議事録に担当・期限が書かれていないものは空欄にする。勝手に決めない
- FAQ: drafts/faq_YYYYMMDD.md。「## Q. 質問」「回答」の繰り返し。回答が割れている質問は載せず、一覧で報告する
## 書き出してはいけない情報
- 個人の評価・人事・健康・家庭の事情、給与や契約条件、顧客の個人情報
- 「社外秘」「未発表」と書かれた話題
- 見つけたら下書きから除き、除いたことだけを報告する(中身は書かない)
## write 系スクリプト
- まず --commit なしでドライランし、出力を人に見せて確認を求める
- 人が「OK」と言ったときだけ --commit を付けて再実行する「推測で補わない」「根拠の日時を付ける」は、要約の品質を上げるうえで効果の大きい指示です。根拠が付いていれば、確認する人は怪しい項目だけ元のスレッドを見に行けばよく、確認の手間が「全部読み直す」から「気になる所だけ見る」に減ります。
.claude/settings.json の permissions 例
CLAUDE.mdは「お願い」、permissionsは「鍵」です。allowは確認なしで実行、askは毎回確認、denyは禁止。守らせたいことは設定で強制します。
{
"permissions": {
"allow": [
"Bash(python scripts/read/slack_fetch.py:*)",
"Bash(python scripts/read/notion_overdue.py:*)",
"Bash(python scripts/read/notion_page_text.py:*)",
"Read(./data/**)",
"Read(./drafts/**)",
"Edit(./drafts/**)"
],
"ask": [
"Bash(python scripts/write/notion_publish_md.py:*)",
"Bash(python scripts/write/notion_create_tasks.py:*)",
"Bash(python scripts/write/slack_remind.py:*)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Edit(./scripts/**)",
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(env:*)",
"Bash(printenv:*)",
"Bash(python -c:*)"
]
}
}- read系はallow、write系はask:ディレクトリを分けたおかげで、設定がそのまま「読むのは自由、書くのは確認」という方針の表明になります。write系はドライランも含めて確認が出ますが、「ドライランの結果を見てから
--commitを承認する」流れになるので、むしろ自然です - drafts/ だけ書き込みをallow:
Editのルールは、ファイルを書き換える組み込みツール全般(新規作成を含む)に効きます。Claudeが自由に書けるのは下書き置き場だけ、という状態にします - scriptsの編集はdeny:運用中にClaudeが「許可リストのチェックを外せば読めます」とスクリプトを書き換える、という事態を防ぎます。スクリプトの改修は別の作業として、ブランチを切ってレビューする運用にします
- curl・env・printenv・
python -cはdeny:トークンを表示したり、スクリプトを迂回してAPIを直接叩いたりする抜け道を塞ぎます。ReadのdenyはClaudeのファイル読み取りツールに効くもので、Bash経由の読み取りまでは止めないため、Bash側もallowを絞るのが基本です

Claude Codeとの対話例
プロジェクトのディレクトリでclaudeを起動して頼みます。以下は流れのイメージです(実際の出力は毎回異なります)。
例1:Slackの1週間分を要約してNotionの週報DBへ
> #proj-renewal(C0123GENERAL)の今週分を週報にして、Notionの週報DBに登録して。
● Bash(python scripts/read/slack_fetch.py C0123GENERAL 7)
⎿ 86 messages -> data/slack_C0123GENERAL_20260926.json
● Read(data/slack_C0123GENERAL_20260926.json)
● Write(drafts/weekly_20260926.md)
下書きを drafts/weekly_20260926.md に作成しました。
- 決まったこと 3件 / 進行中 5件 / 困りごと 2件 / 来週の予定 4件
- 09/23 の「〇〇さんの異動」に関するやり取りは、人事に関わるため除外しました
- 「リリース日を10/15にする」は賛成が多いものの決定の発言がないため「検討中」に入れています
内容を確認して、問題なければ登録します。
> 困りごとの2件目は解決済みなので消して。あとはOK。
● Edit(drafts/weekly_20260926.md)
● Bash(python scripts/write/notion_publish_md.py weekly drafts/weekly_20260926.md)
⎿ (dry-run) ...
● Bash(python scripts/write/notion_publish_md.py weekly drafts/weekly_20260926.md --commit)
← ask 設定のため承認を求められる
⎿ created: https://www.notion.so/xxxxxxxx人がやったのは、最初の一言・下書きへのひと言の修正・登録の承認だけです。「除外したこと」と「判断に迷ったこと」をClaudeが自分から報告してくれるのは、CLAUDE.mdに書いたルールの効果です。
例2:議事録からToDoを起票
> 今日の定例の議事録(page_id: 1a2b...)からToDoを抜き出して、タスクDBに起票して。
● Bash(python scripts/read/notion_page_text.py 1a2b...)
● Write(drafts/todos.json)
ToDoを6件抽出しました(drafts/todos.json)。
| # | ToDo | 担当 | 期限 |
| 1 | 新料金表のドラフト作成 | 佐藤 | 2026-10-02 |
| 2 | 取引先A社への日程再調整の連絡 | (議事録に記載なし) | (記載なし) |
...
2件目は担当・期限が議事録にないため空欄です。起票前に決めてください。
> 2件目は田中さん、期限は来週月曜で。
● Edit(drafts/todos.json)
● Bash(python scripts/write/notion_create_tasks.py drafts/todos.json --commit) ← 承認
⎿ 6 tasks creatednotion_overdue.py→slack_remind.py(ドライラン)→ 気になるタスクの補足、まで進めてくれます。送信の承認は、その補足を見てからで十分です。
Notion公式MCPなど、MCPサーバーという選択肢
MCP(Model Context Protocol)は、AIアプリと外部ツールをつなぐ共通規格です。執筆時点で、Notionは公式のMCPサーバー(Notion側がホストするリモート型)を公開しており、Claude Codeからは次のように登録できます(URLと認証方式は公式ドキュメントで確認してください)。
claude mcp add --transport http notion https://mcp.notion.com/mcp
# Claude Code 内で /mcp を開き、認証(ブラウザでNotionにログインして許可)と接続状況を確認Slackについても、Slack自身が提供するものを含めMCPサーバーが公開されています(提供形態・対応プラン・必要な管理者承認は執筆時点で公式を確認してください)。では、この記事の「自作スクリプト」構成とどう使い分けるべきでしょうか。
| 観点 | 公式・公開MCPサーバー | 自作スクリプト+permissions |
|---|---|---|
| 始めやすさ | ◎ 登録して認証するだけ | △ スクリプトを書いてレビューする手間 |
| 見える範囲 | 認証したユーザーの権限に近い範囲になりやすい(仕様は要確認) | インテグレーションに接続したページ・許可リストのチャンネルだけ |
| 書き込みの制御 | ツール単位で許可/確認を設定 | 件数上限・対象DB・形式チェックをコードで強制できる |
| 再現性 | 毎回Claudeがツールの呼び方を考える | 同じ入力なら同じ書き込み |
| 向いている作業 | 「あのページどこ?」「このDBの構造を見て」など探索的な作業 | 週報・リマインドなど毎週決まった形で回す定型処理 |
おすすめは併用です。スクリプトを書く段階では、MCPでDBのプロパティ構成を確認しながらClaude Codeにスクリプトを書かせ、できあがった定型処理はスクリプトで回す。MCPのツールも、Claude Codeのpermissionsでmcp__notion__...のようなツール名単位でallow/ask/denyを設定できるので、書き込み系のツールはaskにしておきます。--scope projectを付けて登録すると.mcp.jsonに保存され、チームで同じ設定を共有できます。

Skills化と定期実行(ヘッドレス)
Skills:週報づくりを「手順パッケージ」にする
毎週同じ手順を踏む作業は、.claude/skills/<name>/SKILL.mdにまとめておきます。フロントマターのdescriptionに「いつ使うか」を具体的に書いておくと、Claude Codeは関連する依頼を受けたときにこの手順を読み込みます。
---
name: weekly-digest
description: Slackの指定チャンネルの1週間分を要約し、Notionの週報DBに「要確認」で登録する。週報、今週のまとめ、チャンネルの要約を頼まれたときに使う。
---
# 週報づくりの手順
1. 対象チャンネルIDを確認する(.env の SLACK_ALLOWED_CHANNELS にあるものだけ)
2. `python scripts/read/slack_fetch.py <channel_id> 7` を実行する
3. CLAUDE.md の「下書きのルール」に従い drafts/weekly_YYYYMMDD.md を書く
4. 除外した話題(件数と理由の種類だけ)と、判断に迷った項目を人に報告する
5. 人のOKが出たら notion_publish_md.py weekly をドライラン → --commit の順で実行するCLAUDE.mdは「いつでも守るルール」、Skillは「特定の作業のときだけ使う手順」です。FAQ下書き用、ToDo起票用も同じ形でSkillにしておくと、「FAQ作って」の一言で同じ品質の下書きが出てくるようになります。
ヘッドレス実行:金曜の夕方に下書きまで作っておく
claude -p "プロンプト"で非対話(ヘッドレス)実行ができます。途中で人が確認できないため、ヘッドレスでは write 系を一切許可せず、「下書きを作って、できたことを通知する」までに留めるのがおすすめです。
#!/usr/bin/env bash
# 毎週金曜 17:00 に cron 等から実行。下書き作成までで止める
set -euo pipefail
cd "$(dirname "$0")/.."
claude -p "weekly-digest の手順1〜4を実行して、C0123GENERAL の週報の下書きを作って。登録はしないこと。" \
--allowedTools "Bash(python scripts/read/slack_fetch.py:*)" "Read" "Edit(./drafts/**)" \
--output-format json > "logs/weekly_$(date +%Y%m%d).json"
# 下書きができたことだけを担当者に知らせる(本文は送らない)
curl -s -X POST -H 'Content-type: application/json' \
--data '{"text":"今週の週報の下書きができました。drafts/ を確認してください"}' \
"$SLACK_NOTIFY_WEBHOOK_URL"通知だけは、読み取り権限を持たないIncoming Webhookをシェルスクリプト側から送っています(Claudeにcurlを許可しているわけではありません)。月曜の朝に担当者がclaudeを開き、「週報の下書きを確認した。登録して」と言えば残りが進みます。CIで回す場合は、GitHub Actions用のanthropics/claude-code-actionを使う方法もあります。トークン類はリポジトリのSecretsに入れ、ログに出さないようにしてください。CLIのオプション、料金・プランは更新が速いので、執筆時点の情報として公式ドキュメントで確認してください。
「作るのはAI、出すのは人」はサイト運用と同じ型
当サイト(AIエージェント研究所)の運用では、記事の追加や修正の大半をClaude Codeとの共同作業で行っていますが、変更は必ず作業ブランチ→PR→人が確認してマージという流れを通します。フッターの表記漏れがあればビルドスクリプトがexit 1で止まるようにし、秘密情報は.gitignoreで配信・コミットの対象から外しています。
SlackやNotionへの書き込みも同じ考え方で設計できます。下書き(drafts/)=ブランチ、ドライラン=差分表示、--commitの承認=マージです。仕組みの詳細は【自社事例】AIエージェントでメディアサイトを運用する仕組みで公開しています。
注意点・安全設計
個人情報・社外秘
- DMは対象外:スコープで読めなくし、スクリプトでもチャンネル種別を確認して止める。DMは「1対1の約束」で書かれた会話なので、要約して共有すれば信頼を失います
- 公開範囲が変わることを意識する:公開チャンネルの会話でも、要約してNotionの全社ページに置けば、読む人の範囲が変わります。週報DBの閲覧権限は、元チャンネルの参加者とそろえるのが無難です
- 書き出してはいけない情報をCLAUDE.mdに明記し、除外したことを報告させる。完全には防げないので、最後は人の確認で止める
- data/ と drafts/ はGitにコミットしない。社内の会話そのものです。保存期間を決めて定期的に消します
- Claude Codeに渡したデータは、利用しているプラン・契約のデータ取り扱い条件に従って処理されます。社内ルールで外部AIに渡してよい情報の範囲を決め、Slack・Notionの管理者とも合意しておきましょう。ワークスペースによってはアプリのインストールに管理者承認が必要です
投稿前に人が確認する
- write系はドライラン既定+permissionsのask。ヘッドレスではwrite系を許可しない
- 件数上限(ToDoは15件まで、など)をコードで持つ。暴走したときの被害の上限を決めておく
- Slackへの投稿は、同じ内容を二重に送らないよう、送信したら
data/に記録を残す(再実行で重複しないように)
レート制限
- Slack:Web APIはメソッドごとに「Tier」で上限が決まっており、
chat.postMessageは1チャンネルあたりおおむね1秒1通が目安とされています。また執筆時点で、Marketplace外で配布されるアプリについてはconversations.historyなどの上限を大幅に厳しくする変更が告知されています。社内向けに自作したアプリの扱いを含め、最新の条件を公式で確認してください。slack_sdkのRateLimitErrorRetryHandlerを付けておくと、429が返ったときにRetry-Afterに従って待ってくれます - Notion:インテグレーションあたり平均毎秒3リクエスト程度が目安とされ、超えると429が返ります(執筆時点)。
notion-client自体は自動リトライしない前提で、大量に起票するときはループ内で間隔を空けるか、件数上限で抑えます
他社の例では、Shopifyが社内でAI活用を前提とする方針をCEOの社内メモで示した(2025年)と報じられるなど、「社内業務にAIを組み込む」こと自体はもはや特別ではなくなりました。差がつくのは、どこまで任せ、どこで人が止めるかを設計しているかどうかです。
よくある質問
Slackのプライベートチャンネルも要約できる?
技術的には groups:history スコープを付け、アプリをそのチャンネルに招待すれば読めます。ただし、プライベートチャンネルは「参加者だけに見せる」前提で書かれた会話です。要約を別の場所(Notionなど)に書き出すと公開範囲が変わってしまうため、対象にする場合はチャンネルの参加者の合意と、書き出し先の閲覧権限を先に決めてください。まずは公開チャンネルだけで始めるのがおすすめです。
Notion APIで「ページが見つからない(object_not_found)」と言われる
最も多い原因は、対象のページやデータベースをインテグレーションに共有(接続)していないことです。Notionの内部インテグレーションは、ワークスペース全体ではなく、明示的に接続されたページとその子ページしか見えません。ページ右上のメニューから接続を追加し、IDが正しいか(URLの32桁の部分か)も確認してください。
自作スクリプトとNotion公式のMCPサーバー、どちらを使うべき?
対話しながらページを探したり下書きを作ったりする用途なら、公式MCPサーバーが手軽です。一方、毎週決まった形式で登録する定期処理や、書き込み先と件数を厳密に制限したい処理は、レビュー済みの自作スクリプトのほうが再現性と安全性を確保しやすくなります。両方を併用し、定型処理はスクリプト、探索的な作業はMCP、と使い分けるのが現実的です。
要約やリマインドを完全自動で投稿してもいい?
最初はおすすめしません。要約は誤読や言い過ぎが起こり得ますし、リマインドは担当者の取り違えがあると信頼を損ねます。はじめは下書きを作るところまで自動化し、人が確認してから投稿する運用にしてください。数週間運用して修正がほとんど不要になってから、影響の小さい処理から自動投稿に切り替えるのが安全です。
まとめ|「読むのは自動、書くのは人が確認してから」
SlackとNotionをClaude Codeにつなぐと、「流れていく会話」を「残る知識」に変える作業——週報、リマインド、ToDo起票、FAQづくり——を、毎週の仕組みとして回せるようになります。成功のポイントは、トークンのスコープとインテグレーションの共有範囲で見える範囲を絞り、APIはレビュー済みのスクリプトに任せ、Claude Codeには要約と下書きを、投稿の判断は人に残すことです。まずは公開チャンネル1つでslack_fetch.pyを動かし、週報の下書きを1本作ってみるところから始めてみてください。APIそのものの仕組みから学びたい方はIT基礎シリーズ第4章「API・JSON入門」がおすすめです。
業務自動化のスキルを、キャリアと収入につなげる
APIとAIエージェントで業務を自動化できる人材は、社内でも社外でも求められています。次の一歩に役立つ記事です。
- AIで独立するには?未経験から案件を取る6ステップ【2026】:自動化・開発スキルを案件にする。フリーランスエージェントの選び方も
- 仕事用キーボード・マウスおすすめ3選【2026】:長時間のコーディング・プロンプト入力を快適に