Claude Code×HubSpot・Salesforce API【2026】営業データの分析とCRM運用をAIエージェントで
「この商談、最後に連絡したのはいつ?」「金額が空欄の商談が多くて、パイプラインの数字が信用できない」「同じ会社の担当者が3件登録されている」——CRMは入れた瞬間から、データをきれいに保ち、動かし続ける運用が課題になります。この記事では、Claude Code(ターミナルで動くAnthropicのコーディングエージェント)をHubSpot CRM APIとSalesforce(sf CLI・SOQL)につなぎ、停滞商談の抽出から週次サマリー、重複の候補出し、活動記録の下書きまでを任せる構成を、コードと設定ファイル付きで解説します。CRMの更新は人が承認したものだけ、監査ログ付きで行う前提です。
- HubSpotはプライベートアプリのトークンでCRM API v3(
/crm/v3/objects/dealsなど)を読む。読み取り用と書き込み用のトークンを分ける - Salesforceはsf CLIでログインし、
sf data queryでSOQLを実行。認証をCLIに任せられるのでClaude Codeから扱いやすい。まずSandboxで - 「停滞」「必須項目」の定義はCLAUDE.mdと設定ファイルに書き、判定はスクリプトで決定的に。Claude Codeは次アクション案や活動記録の下書きを担当
- 更新・削除・統合はpermissionsでdeny、承認済みの登録だけask+監査ログ。最後に通すのは人
何ができるか:営業・CRM運用の5つのユースケース
営業部門でAIエージェントが効くのは、CRMに溜まったデータを読み、「次に何をすべきか」を整理する作業です。レコードを書き換える作業は、最後まで人の承認を通します。
| ユースケース | 使うデータ | Claude Codeの役割 | 人の役割 |
|---|---|---|---|
| ① 停滞している商談の抽出と次アクション案 | 商談(ステージ・最終活動日・金額) | 担当者別の整理、次アクション案 | アクションの実行 |
| ② パイプラインの週次サマリー | 商談(ステージ別件数・金額) | 前週比の整理、所見 | 営業会議での判断 |
| ③ 重複コンタクトの検出と統合“候補”リスト | コンタクト(メール・電話・氏名・会社) | 候補の妥当性チェック、統合方針の案 | CRMの標準機能で統合 |
| ④ 商談メモからの活動記録の下書き | 手元の商談メモ | 要点・次アクションの構造化 | 確認・承認 |
| ⑤ データ品質チェック(必須項目の空欄) | 商談・コンタクト | 担当者別の集計、入力依頼文の下書き | 入力・修正 |


SalesforceはAIエージェント機能「Agentforce」を、HubSpotはAI機能群「Breeze」を発表しており(いずれも2024年)、CRMベンダー自身もエージェント化を進めています。自前の連携は、それらと競うものではなく、自社ルールでの点検と下書きを、手元で透明に回すための選択肢と考えるのがよいでしょう。APIの基本から確認したい方は、初心者向けのIT基礎シリーズ第4章「API・JSON入門」をどうぞ。
準備と認証:プライベートアプリ・sf CLI・Sandbox
HubSpot:プライベートアプリを「読み取り用」と「書き込み用」に分ける
HubSpotでは、設定画面からプライベートアプリを作成し、必要なスコープを選ぶとアクセストークンが発行されます。リクエストにはAuthorization: Bearer <トークン>ヘッダーを付けます。この記事ではアプリを2つ作ります。
| アプリ | スコープ(例) | 使う場所 |
|---|---|---|
| 読み取り用 | crm.objects.deals.read、crm.objects.contacts.read、crm.objects.owners.read | 取得・分析スクリプト(Claude Codeが日常的に実行) |
| 書き込み用 | メモ(活動記録)の登録に必要な最小限の書き込みスコープ | 承認済みの下書きを登録するスクリプトだけ(実行は毎回確認) |
トークンを分けておけば、日常的に動く分析スクリプトが誤って書き込むことはAPI側で不可能になります。必要なスコープの正確な名前は、公式ドキュメントで確認してください。
Salesforce:sf CLIでSandboxにログイン
Salesforceは、公式のSalesforce CLI(sf)を使うのが扱いやすい方法です。ブラウザでログインすると認証情報をCLIが管理してくれるため、スクリプトやClaude Codeがトークンを直接扱わずに済みます。まずは本番と切り離されたSandboxにログインします。
npm install -g @salesforce/cli
# Sandbox にブラウザでログイン(本番は login.salesforce.com / My Domain のURL)
sf org login web --alias sbx --instance-url https://test.salesforce.com
sf org list- ログインは人が行う:Claude Codeにはログイン済みのエイリアス(
sbx)でクエリだけを実行させます - 見える範囲=ログインしたユーザーの権限:Salesforceは共有ルールや項目レベルセキュリティで見えるデータが決まります。分析用には参照権限だけの権限セットを持つユーザーを用意するのが理想です
- 定期実行では専用の連携ユーザー:無人で動かす場合は、接続アプリ(または外部クライアントアプリ)とJWTなどのOAuthフローで、API専用の統合ユーザーを使う構成を検討します
- エディションによってはAPIが利用できない場合があります。ご利用の契約で確認してください
Pythonから直接REST APIを呼びたい場合は、コミュニティ製のライブラリsimple-salesforceもよく使われます。HubSpotには公式のPythonクライアントhubspot-api-clientがありますが、この記事では挙動が追いやすいrequestsで書きます(Python 3.11以上を想定)。
プロジェクト構成
営業データ用のリポジトリを1つ作り、そこでClaude Codeを起動します。「読む」「下書く」「承認して書く」の置き場をディレクトリで分けているのがポイントです。
sales-ops/
├── CLAUDE.md # 項目定義・ステージ定義・ルール
├── .env # HubSpotトークン(Git・Claudeの対象外)
├── .claude/settings.json # permissions / hooks
├── .claude/skills/weekly-pipeline/SKILL.md
├── config/stage_rules.json # ステージごとの「停滞」日数・必須項目
├── scripts/hubspot/ # client.py, fetch_deals.py, fetch_contacts.py
├── scripts/salesforce/ # sf_query.py(sf CLI のラッパー)
├── scripts/analyze/ # stale_deals.py, find_duplicates.py, check_required.py
├── scripts/write/ # register_activity.py(承認済みの下書きだけを登録。ask)
├── reports/ # 取得・分析結果(Git管理外)
├── notes/inbox/ # 商談メモ(人が置く)
├── drafts/activities/ # 活動記録の下書き(Claudeが書く)
├── approved/ # 人が確認して移した下書き(Claudeは編集不可)
└── logs/audit.jsonl # 書き込みの監査ログapproved/は人だけが置けるディレクトリです(Claude Codeには編集を禁止します)。「Claudeが下書きし、人がapprovedに移したものだけが登録される」という流れを、ファイルの置き場所で表現しています。
HubSpot CRM API v3:商談・コンタクトの取得と分析
クライアント:一覧のページングと検索
一覧はGET /crm/v3/objects/{object}で取得し、レスポンスのpaging.next.afterを次のリクエストに渡して全件をたどります。条件で絞るときはPOST /crm/v3/objects/{object}/searchを使います。
"""HubSpot CRM API v3 の薄いクライアント(読み取り用トークン)"""
import os
import time
import requests
from dotenv import load_dotenv
load_dotenv()
BASE = "https://api.hubapi.com"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['HUBSPOT_READ_TOKEN']}"
def _request(method: str, path: str, **kwargs) -> dict:
for attempt in range(5):
resp = session.request(method, BASE + path, timeout=30, **kwargs)
if resp.status_code == 429 or resp.status_code >= 500:
time.sleep(2 ** attempt) # レート制限・一時エラーは待って再試行
continue
resp.raise_for_status()
return resp.json()
raise RuntimeError(f"HubSpot API: retries exhausted ({path})")
def list_objects(obj: str, properties: list[str]):
"""GET /crm/v3/objects/{obj} を paging.next.after でたどって全件返す"""
params = {"limit": 100, "properties": ",".join(properties)}
while True:
data = _request("GET", f"/crm/v3/objects/{obj}", params=params)
yield from data["results"]
after = data.get("paging", {}).get("next", {}).get("after")
if not after:
return
params["after"] = after
def search(obj: str, filters: list[dict], properties: list[str]):
"""POST /crm/v3/objects/{obj}/search(1クエリで取れる件数に上限があるので範囲を絞って使う)"""
body = {"filterGroups": [{"filters": filters}], "properties": properties, "limit": 100}
while True:
data = _request("POST", f"/crm/v3/objects/{obj}/search", json=body)
yield from data["results"]
after = data.get("paging", {}).get("next", {}).get("after")
if not after:
return
body["after"] = after
time.sleep(0.3) # 検索APIは通常のAPIより制限が厳しい検索APIは便利ですが、1回の検索でたどれる件数に上限があり、レート制限も通常のAPIより厳しいという特徴があります(具体的な値は公式で確認)。全件を定期的に取るなら一覧API、差分だけ取るなら検索API(例:hs_lastmodifieddateが1週間以内)と使い分けるのが実践的です。
商談とパイプライン定義を取得する
"""商談とパイプライン定義を取得して reports/ に保存する"""
import csv
import json
from client import _request, list_objects
PROPS = ["dealname", "amount", "dealstage", "pipeline", "closedate",
"hubspot_owner_id", "notes_last_updated", "hs_lastmodifieddate"]
# ステージIDとラベル、クローズ済みかどうか(metadata.isClosed)を保存
pipelines = _request("GET", "/crm/v3/pipelines/deals")["results"]
stages = {s["id"]: {"label": s["label"], "closed": s["metadata"].get("isClosed") == "true"}
for p in pipelines for s in p["stages"]}
json.dump(stages, open("reports/stages.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
owners = {o["id"]: f"{o.get('lastName', '')} {o.get('firstName', '')}".strip()
for o in _request("GET", "/crm/v3/owners", params={"limit": 500})["results"]}
with open("reports/deals.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["id", *PROPS, "stage_label", "is_closed", "owner_name"])
for d in list_objects("deals", PROPS):
p = d["properties"]
stage = stages.get(p.get("dealstage"), {"label": "(不明)", "closed": False})
w.writerow([d["id"], *[p.get(k) or "" for k in PROPS],
stage["label"], stage["closed"], owners.get(p.get("hubspot_owner_id"), "")])ここでパイプライン定義も一緒に保存しているのが大事な点です。HubSpotのdealstageにはappointmentscheduledのような内部IDが入っていて、画面の表示名とは異なります。カスタムのステージでは数字のIDになることもあります。IDとラベル、クローズ済みかどうか(metadata.isClosed)の対応表をreports/stages.jsonに残しておけば、Claude Codeがレポートを書くときに表示名で書けます。
停滞商談と必須項目の空欄を抽出する
「停滞」の定義は、営業部門と合意して設定ファイルに書きます。ステージによって適切な間隔は違うので、ステージごとに日数を持たせます。
{
"_comment": "キーは HubSpot のステージID(reports/stages.json で確認)。値は営業部門と合意した定義",
"appointmentscheduled": { "stale_days": 7, "required": ["amount", "closedate", "hubspot_owner_id"] },
"qualifiedtobuy": { "stale_days": 10, "required": ["amount", "closedate", "hubspot_owner_id"] },
"presentationscheduled":{ "stale_days": 14, "required": ["amount", "closedate", "hubspot_owner_id"] },
"contractsent": { "stale_days": 7, "required": ["amount", "closedate", "hubspot_owner_id"] }
}"""停滞商談と必須項目の空欄を、config/stage_rules.json の定義で抽出する"""
import csv
import json
from datetime import datetime, timezone
rules = json.load(open("config/stage_rules.json", encoding="utf-8"))
now = datetime.now(timezone.utc)
def days_since(value: str) -> int | None:
if not value:
return None
return (now - datetime.fromisoformat(value.replace("Z", "+00:00"))).days
stale, missing = [], []
for d in csv.DictReader(open("reports/deals.csv", encoding="utf-8")):
rule = rules.get(d["dealstage"])
if d["is_closed"] == "True" or rule is None:
continue
# 最終活動日(notes_last_updated)を優先。なければ最終更新日で代用し、その旨を残す
basis = "last_activity" if d["notes_last_updated"] else "last_modified"
idle = days_since(d["notes_last_updated"] or d["hs_lastmodifieddate"])
if idle is not None and idle >= rule["stale_days"]:
stale.append([d["id"], d["dealname"], d["stage_label"], d["owner_name"], d["amount"], idle, basis])
empty = [k for k in rule["required"] if not d.get(k)]
if empty:
missing.append([d["id"], d["dealname"], d["stage_label"], d["owner_name"], "|".join(empty)])
with open("reports/stale_deals.csv", "w", newline="", encoding="utf-8") as f:
csv.writer(f).writerows([["id", "deal", "stage", "owner", "amount", "idle_days", "basis"], *stale])
with open("reports/missing_fields.csv", "w", newline="", encoding="utf-8") as f:
csv.writer(f).writerows([["id", "deal", "stage", "owner", "missing"], *missing])
print(f"stale={len(stale)} missing={len(missing)}")判定の基準日は最終活動日(notes_last_updated:メール・通話・会議などの記録日)を優先し、空なら最終更新日で代用して、その旨をbasis列に残します。最終更新日は自動化ルールによる更新でも変わるため、「最近更新されている=動いている」とは限らないからです。こうした判断の理由を列として残すと、Claude Codeがレポートに注記を付けられ、読む人の誤解も防げます。
重複コンタクトの“候補”を出す
コンタクトはfetch_deals.pyと同じ要領で、email, phone, firstname, lastname, company, createdateをreports/contacts.csvに保存しておきます(fetch_contacts.py)。重複の判定は、メール(小文字化)・電話番号(数字だけの下10桁)・氏名+会社名の3つの観点で行います。
"""重複コンタクトの「候補」を出す。メールはマスクして出力し、統合はしない"""
import csv
import re
from collections import defaultdict
def norm_email(e: str) -> str:
return (e or "").strip().lower()
def norm_phone(p: str) -> str:
digits = re.sub(r"\D", "", p or "")
return digits[-10:] if len(digits) >= 10 else "" # 国番号・ハイフンの違いを吸収
def mask(e: str) -> str:
local, _, domain = e.partition("@")
return f"{local[:1]}***@{domain}" if domain else ""
groups = defaultdict(set)
# fetch_contacts.py が保存した id, email, phone, firstname, lastname, company, createdate の列
contacts = {c["id"]: c for c in csv.DictReader(open("reports/contacts.csv", encoding="utf-8"))}
for cid, c in contacts.items():
if norm_email(c["email"]):
groups[("email", norm_email(c["email"]))].add(cid)
if norm_phone(c["phone"]):
groups[("phone", norm_phone(c["phone"]))].add(cid)
name = (c["lastname"] + c["firstname"]).replace(" ", "").replace(" ", "")
if name and c["company"]:
groups[("name+company", name + "/" + c["company"].strip())].add(cid)
with open("reports/duplicate_candidates.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["reason", "contact_ids", "masked_emails", "created_dates"])
for (reason, _), ids in groups.items():
if len(ids) > 1:
ids = sorted(ids)
w.writerow([reason, "|".join(ids),
"|".join(mask(norm_email(contacts[i]["email"])) for i in ids),
"|".join(contacts[i]["createdate"][:10] for i in ids)])出力のメールアドレスはt***@example.comのようにマスクしています。重複の妥当性を判断するのにフルのアドレスは不要で、Claude Codeが読むファイルから個人情報を減らせるからです。統合(merge)はAPIでも可能ですが、どちらの値を残すか、関連する商談や活動履歴をどう扱うかは業務判断です。この記事では統合をdenyにし、候補リストと方針の案までをClaude Codeに任せます。HubSpotにもSalesforceにも重複管理の標準機能があるので、実際の統合はそちらで担当者が行います。
Salesforce:sf CLIとSOQLで同じことをする
Salesforceでは、同じ分析をSOQL(Salesforceのデータを取得するためのSQLに似た言語)で書けます。sf data queryに--result-format csvを付ければそのままCSVで保存できるので、単純な抽出ならスクリプトすら不要です。
# 停滞している商談(オープンで、14日以上活動がない)
sf data query --target-org sbx --result-format csv --query "
SELECT Id, Name, StageName, Amount, CloseDate, Owner.Name, LastActivityDate
FROM Opportunity
WHERE IsClosed = false
AND (LastActivityDate = null OR LastActivityDate < LAST_N_DAYS:14)
ORDER BY Amount DESC NULLS LAST" > reports/sf_stale_opps.csv
# パイプラインの週次サマリー(ステージ別の件数と金額)
sf data query --target-org sbx --query "
SELECT StageName, COUNT(Id) cnt, SUM(Amount) total
FROM Opportunity WHERE IsClosed = false GROUP BY StageName"
# 必須項目の空欄チェック
sf data query --target-org sbx --result-format csv --query "
SELECT Id, Name, StageName, Owner.Name FROM Opportunity
WHERE IsClosed = false AND (Amount = null OR CloseDate = null OR NextStep = null)" > reports/sf_missing.csv
# 同じメールアドレスのコンタクト(重複候補)
sf data query --target-org sbx --query "
SELECT Email, COUNT(Id) c FROM Contact
WHERE Email != null GROUP BY Email HAVING COUNT(Id) > 1"SOQLにはLAST_N_DAYS:14のような日付リテラルがあり、「14日以上活動がない」を簡潔に書けます。LastActivityDateは活動(ToDo・行動)の記録から自動で決まる項目です。NextStep(次のステップ)は標準項目ですが、組織によってはカスタム項目で管理していることもあるので、CLAUDE.mdの項目定義に合わせて書き換えます。
定型のクエリはqueries/*.soqlとしてファイルに置き、次のようなラッパーで実行すると、クエリ自体をGitでレビューできます。
"""sf CLI で SOQL を実行し、JSON の records を CSV に保存する(認証は sf CLI に任せる)"""
import csv
import json
import subprocess
import sys
ORG = "sbx" # 本番に切り替えるときは人が明示的に変更する
def run_soql(soql: str) -> list[dict]:
out = subprocess.run(
["sf", "data", "query", "--target-org", ORG, "--query", soql, "--json"],
capture_output=True, text=True, check=True,
)
records = json.loads(out.stdout)["result"]["records"]
for r in records:
r.pop("attributes", None)
owner = r.pop("Owner", None) # リレーション項目はネストで返るので平らにする
if owner:
r["OwnerName"] = owner.get("Name")
return records
if __name__ == "__main__":
soql_file, out_csv = sys.argv[1], sys.argv[2] # 例: queries/stale.soql reports/sf_stale.csv
rows = run_soql(open(soql_file, encoding="utf-8").read())
with open(out_csv, "w", newline="", encoding="utf-8") as f:
if rows:
w = csv.DictWriter(f, fieldnames=list(rows[0]))
w.writeheader()
w.writerows(rows)
print(f"{len(rows)} records -> {out_csv}")リレーション項目(Owner.Name)はJSONでネストして返るため、CSVにする前に平らにしています。件数が多い場合は、Bulk API系のコマンドやオプションの利用を検討してください(仕様は公式で確認)。Salesforceには1日あたりのAPIリクエスト数の上限があり、ほかの連携ツールと共有しています。定期実行の頻度は、組織全体の利用状況を見て決めましょう。
商談メモから活動記録の下書き、承認して登録
商談後のメモ(箇条書きでも音声の文字起こしでも)をnotes/inbox/に置き、Claude Codeに活動記録の下書きを作らせます。下書きはJSONで、CRMに登録する項目の形にそろえます。
{
"deal_id": "1234567890",
"timestamp": "2026-09-25T06:00:00Z",
"body": "【打ち合わせ】先方の情シス部門と導入範囲を確認。\n・決裁は10月の定例会議\n・懸念点:既存システムとのデータ連携\n【次アクション】連携方式の資料を10/2までに送付(担当:営業)",
"next_step": "連携方式の資料を送付",
"next_step_due": "2026-10-02",
"source": "notes/inbox/2026-09-25_acme.md"
}人が内容を確認し、必要なら直してapproved/に移します。登録スクリプトはapproved/配下のファイルしか受け付けず、書き込み用トークンで登録したあと、誰が承認したかを監査ログに追記します。
"""approved/ に人が移した活動記録(JSON)だけを HubSpot のメモとして登録し、監査ログを残す"""
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
import requests
from dotenv import load_dotenv
load_dotenv()
draft_path, approver = Path(sys.argv[1]).resolve(), sys.argv[2] # 例: approved/2026-09-25_acme.json yamada
if Path("approved").resolve() not in draft_path.parents:
raise SystemExit("approved/ 配下のファイル(人が承認して移したもの)だけ登録できます")
draft = json.load(open(draft_path, encoding="utf-8"))
body = {
"properties": {"hs_timestamp": draft["timestamp"], "hs_note_body": draft["body"]},
"associations": [{
"to": {"id": draft["deal_id"]},
"types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 214}], # メモ→商談(公式で確認)
}],
}
resp = requests.post(
"https://api.hubapi.com/crm/v3/objects/notes",
headers={"Authorization": f"Bearer {os.environ['HUBSPOT_WRITE_TOKEN']}"},
json=body, timeout=30,
)
resp.raise_for_status()
with open("logs/audit.jsonl", "a", encoding="utf-8") as log:
log.write(json.dumps({
"at": datetime.now(timezone.utc).isoformat(), "action": "create_note",
"deal_id": draft["deal_id"], "note_id": resp.json()["id"],
"file": draft_path.name, "approved_by": approver,
}, ensure_ascii=False) + "\n")
print("registered:", resp.json()["id"])関連付けタイプID(メモ→商談)はHubSpotが定義している値で、オブジェクトの組み合わせごとに異なります。実装前に公式ドキュメントで確認してください。Salesforceの場合も同じ形で、承認済みファイルからToDo(Task)などを作成するスクリプトを用意し、実行をaskにします。
"status": "approved"と書かせる方式だと、Claude Code自身が書き換えられてしまいます。Claudeが編集できない場所(approved/)にファイルがあることを承認の証拠にすれば、承認の操作は必ず人の手を通ります。さらに登録スクリプトの実行自体もaskなので、二重の関所になります。
CLAUDE.mdに項目定義・ステージ定義を書く
CRMの分析で一番ずれやすいのは、「この項目は何を意味するか」です。「金額」は税込か税抜か、見込みか受注額か。「最終活動日」には何が含まれるか。CLAUDE.md(プロジェクト直下に置くと毎回読み込まれる手順書。/initで雛形を作れます)に、項目定義とステージ定義を書いておきます。
# CLAUDE.md(営業データ分析・CRM運用)
## 目的
- HubSpot / Salesforce のデータを読み、停滞商談・週次サマリー・重複候補・データ品質の点検と、
活動記録の下書きを作る。CRMのデータ更新は、人の承認後に scripts/write/ 経由でのみ行う
## データの取り方
- HubSpot: python scripts/hubspot/fetch_deals.py / fetch_contacts.py → scripts/analyze/*.py
- Salesforce: sf data query(--target-org sbx)または scripts/salesforce/sf_query.py
- 件数・金額の集計は reports/ のCSVを使う。暗算で再集計しない
## 項目定義(HubSpot 内部名 / Salesforce API参照名)
- 商談金額: amount / Amount(税抜。見込み金額であり受注額ではない)
- 完了予定日: closedate / CloseDate
- 最終活動日: notes_last_updated / LastActivityDate(メール・通話・会議の記録日)
- 次のステップ: next_step(カスタム)/ NextStep
## ステージ定義(停滞判定は config/stage_rules.json が正本)
- 初回商談設定 → 要件確認 → 提案 → 契約書送付 → 受注 / 失注
- 「停滞」= 最終活動日からの経過日数がステージごとの stale_days 以上
- ステージIDとラベルの対応は reports/stages.json を参照(IDは表示名と異なる)
## 下書きのルール
- 活動記録: notes/inbox/ のメモから drafts/activities/*.json を作る。approved/ へ移すのは人
- メモにない事実(金額・日付・決裁者)を補わない。不明点は「要確認」と書く
- 次アクション案は「誰が・何を・いつまでに」の形で、最大3つ
## 禁止
- レコードの更新・削除・統合(merge)、オーナー変更、一括インポート
- .env の参照、reports/ の個人情報を外部サービスやチャットに貼ること
permissionsで「読む・下書く・承認して書く」を分ける
.claude/settings.jsonのpermissionsで、3段階をはっきり分けます。allowは確認なし、askは毎回確認、denyは禁止です。
{
"permissions": {
"allow": [
"Bash(python scripts/hubspot/fetch_deals.py:*)",
"Bash(python scripts/hubspot/fetch_contacts.py:*)",
"Bash(python scripts/analyze/stale_deals.py:*)",
"Bash(python scripts/analyze/find_duplicates.py:*)",
"Bash(python scripts/salesforce/sf_query.py:*)",
"Bash(sf data query:*)",
"Read(./reports/**)",
"Read(./notes/**)",
"Edit(./drafts/**)"
],
"ask": [
"Bash(python scripts/write/register_activity.py:*)",
"Edit(./scripts/**)",
"Edit(./config/**)"
],
"deny": [
"Read(./.env)",
"Bash(curl:*)",
"Bash(sf data update:*)",
"Bash(sf data delete:*)",
"Bash(sf data upsert:*)",
"Bash(sf data import:*)",
"Bash(sf data create:*)",
"Bash(sf apex:*)",
"Bash(sf project deploy:*)",
"Bash(sf org display:*)",
"Edit(./approved/**)",
"Edit(./logs/**)"
]
}
}- allow=読む・下書く:取得・分析スクリプト、
sf data query、drafts/への書き込み - ask=承認して書く:登録スクリプトの実行。判定ルール(
config/)やスクリプトの変更も、結果を変えるのでask - deny=させない:
sf data update/delete/upsert/import/create、Apexの実行、メタデータのデプロイ、approved/と監査ログの編集 sf org displayもdeny:組織の情報とともにアクセストークンが表示されるためです。見落としやすいポイントです
ただし、コマンドの前方一致による制御は万能ではありません。本当の防衛線は、CRM側のユーザー権限とトークンのスコープです。分析に使うユーザー・トークンは参照のみ、書き込み用は別に分ける——この土台の上に、permissionsやhooksで操作の範囲をさらに絞ります。安全設計の全体像はAIエージェントを本番運用する安全設計ガイドで解説しています。
Claude Codeとの対話例
以下は流れのイメージです(実際の出力は毎回異なり、社名・件数は説明用の例です)。
例1:週次サマリーと停滞商談の次アクション案
> 今週のパイプラインのサマリーを作って。停滞している商談は
担当者別に並べて、それぞれ次のアクション案も。
● Bash(python scripts/hubspot/fetch_deals.py)
● Bash(python scripts/analyze/stale_deals.py)
⎿ stale=14 missing=9
● Read(reports/deals.csv) / Read(reports/stale_deals.csv) / Read(config/stage_rules.json)
## パイプライン週次サマリー(数値は reports/ から転記)
| ステージ | 件数 | 金額合計 | 前週比 | ...
## 停滞商談(14件)担当者別
■ 佐藤さん(5件)
- A社 導入案件(提案/活動なし21日)
次アクション案:提案書への質問期限を確認する連絡を、今週中に
- B社(契約書送付/活動なし9日)
次アクション案:法務確認の進捗を先方担当に確認
...
※ 3件は最終活動日が空欄のため、最終更新日で判定しています(basis=last_modified)
※ 必須項目の空欄が9件あります → reports/missing_fields.csv次アクション案は、担当者が採用するかを決める「提案」です。お客様との関係や社内事情はCRMに書かれていないことも多いので、「AIがそう言ったから」ではなく、担当者の判断材料として使います。
例2:商談メモから活動記録を登録するまで
> notes/inbox/2026-09-25_acme.md から活動記録の下書きを作って。
● Read(notes/inbox/2026-09-25_acme.md)
● Read(reports/deals.csv) ← 商談名から deal_id を特定
● Write(drafts/activities/2026-09-25_acme.json)
下書きを作成しました。メモに決裁者の役職がなかったため「要確認」と
しています。内容を確認し、approved/ に移してから登録を指示してください。
> 1行直して approved/ に移した。登録して。承認者は yamada。
● Bash(python scripts/write/register_activity.py approved/2026-09-25_acme.json yamada)
← ask 設定のため、実行前に確認を求められる
⎿ registered: 98765432101MCPサーバーの使いどころ
MCP(Model Context Protocol)はAIアプリと外部ツールをつなぐ共通規格です。Salesforceは開発者向けのSalesforce DX MCP Serverを、HubSpotもCRMデータにアクセスするMCPサーバーを公開しています(いずれも執筆時点。ベータを含む)。
# Salesforce DX MCP Server(執筆時点で公開。オプションは公式READMEで確認)
claude mcp add salesforce -- npx -y @salesforce/mcp --orgs sbx
# HubSpot のMCPサーバー(公開形態・URL・認証方式は公式ドキュメントで確認)
claude mcp add --transport http hubspot <公式ドキュメントに記載のURL>MCPは「この会社の商談の経緯を教えて」のようなその場の調べものに向いています。一方、週次サマリーや停滞判定のような毎回同じ基準で出したい数字は、この記事のようにスクリプトで固定したほうがレビューしやすく、再現性があります。MCPサーバーによってはレコードの作成・更新ツールも含まれるため、Sandboxや参照権限のユーザーで接続し、書き込み系ツールは有効にしないのが原則です。コマンド・URL・オプションは変わりやすいので、導入時に公式ドキュメントで確認してください。
Skills化とヘッドレスでの週次実行
毎週の営業会議の資料づくりは、Skillsにまとめます。.claude/skills/<name>/SKILL.mdに手順を書くと、Claude Codeは関連する依頼のときに読み込んで使います。
---
name: weekly-pipeline
description: HubSpot(またはSalesforce)の商談データから、パイプラインの週次サマリーと停滞商談の一覧・次アクション案を作る。週次の営業会議資料、パイプライン確認、停滞商談の洗い出しを頼まれたときに使う。
---
1. fetch_deals.py → stale_deals.py を実行する
2. reports/weekly_YYYYMMDD.md に次の順で書く
- ステージ別の件数・金額(表)と前週からの変化(前週のファイルがあれば比較)
- 今週クローズ予定の商談 / 停滞商談(担当者別、金額の大きい順)
- 必須項目の空欄(担当者別の件数のみ。個別の指摘は担当者に直接)
3. 次アクション案は「誰が・何を・いつまでに」で1商談1〜2個。事実とAIの提案を分けて書く
4. 個人の連絡先(メール・電話)はレポートに書かない月曜の朝に自動で作るなら、claude -pのヘッドレス実行をcronなどから呼びます。無人で動くので、許可するツールは取得・分析とreports/への書き込みだけに絞り、登録系は含めません。
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
claude -p "weekly-pipeline の手順で今週のサマリーを作成して" \
--allowedTools "Bash(python scripts/hubspot/fetch_deals.py:*)" \
"Bash(python scripts/analyze/stale_deals.py:*)" \
"Read" "Write(./reports/**)" \
--output-format json > "logs/weekly_$(date +%Y%m%d).json"できたサマリーをSlackに投稿したい場合は、Claude Code×Slack・Notion API連携の方法と組み合わせます。チャンネルに流すのは集計と所見までにし、顧客の連絡先や個別の商談メモは載せないようにします。

注意点:個人情報・権限・承認・監査ログ
個人情報
- 取らない・マスクする:分析に不要な連絡先は取得しない。重複チェックのように必要な場合も、出力ではマスクする
- 利用目的とデータの取り扱い条件:顧客の個人情報を扱う以上、個人情報保護法上の利用目的の範囲内か、社内規程でAIサービスへの入力が認められているか、利用するClaudeのプラン・契約のデータ取り扱い条件はどうか、を事前に確認します
- reports/はGitにコミットしない:
.gitignoreに入れ、共有範囲を決めておきます
権限
- 統合ユーザーは「見えすぎ」に注意:全データを参照できるユーザーで分析すると、本来その営業チームが見られない商談までレポートに載ることがあります。レポートの配布先に合わせて取得範囲を絞ります
- トークンは用途ごとに分け、定期的に見直す:退職・異動・プロジェクト終了時に失効できるよう、発行者と用途を記録しておきます
更新は人が承認し、監査ログを残す
- 自動化ルールとの干渉:CRMのワークフローやフローは、項目の更新をきっかけに動きます。1件の更新がメール送信や担当者変更を引き起こすこともあるため、書き込みは必ずSandboxで挙動を確認します
- 監査ログは二重に:CRM側の項目履歴(HubSpotのプロパティ履歴、Salesforceの項目履歴管理や設定変更履歴など。プラン・設定による)に加え、この記事のように自前のログに「誰が承認したか」を残すと、AI経由の更新を後から追跡できます
- 一括更新はしない:件数の多い更新は、影響範囲の確認と戻し方の準備ができるまで行いません
当社が支援の現場で採っている進め方
当社が営業部門へのAIエージェント導入を支援する際は、読み取り専用の週次サマリーから始めることを基本にしています。まず「停滞」や「必須項目」の定義を営業責任者と文章で合意し、それをCLAUDE.mdと設定ファイルに落とします。数週間運用して判定のずれを直したうえで、活動記録の下書きのような「人の承認つきの書き込み」へ進みます。
この「AIが作り、人が通す」流れは、このサイトの運用(Claude Codeが作業ブランチで変更し、人が確認してマージ)と同じ考え方です。詳しくは【自社事例】AIエージェントでメディアサイトを運用する仕組みをご覧ください。
よくある質問
HubSpotとSalesforce、Claude Codeと連携しやすいのはどちらですか?
どちらも連携できます。HubSpotはプライベートアプリのトークンでREST APIを呼ぶ形がシンプルで、Pythonの小さなスクリプトで始めやすいです。Salesforceは公式のsf CLIでログインとSOQLの実行ができるため、Claude Codeからはsf data queryをコマンドとして使うのが扱いやすい方法です。どちらの場合も、取得と集計はスクリプト、解釈と下書きはClaude Codeという役割分担は同じです。
重複コンタクトの統合までClaude Codeに任せてもいいですか?
おすすめしません。統合は元に戻しにくく、どちらのレコードの値を残すか、関連する商談や活動履歴をどう扱うかなど、業務上の判断が必要だからです。Claude Codeには重複の候補リストと統合方針の案までを作らせ、実際の統合はCRMの標準機能で担当者が確認しながら行う運用が安全です。
商談メモや顧客情報をClaude Codeに読ませても大丈夫ですか?
社内規程と、利用しているClaudeのプラン・契約のデータ取り扱い条件を確認したうえで、必要最小限にとどめるのが原則です。分析に不要な連絡先は取得しない、出力ではメールアドレスをマスクする、reportsフォルダをGitにコミットしないといった工夫で、AIが触れる個人情報を減らせます。
Sandboxで試してから本番に切り替えるときの注意点は?
本番では、実際の営業担当者のデータとワークフローや自動化ルールが動いています。まずは読み取りだけで本番に接続し、Sandboxと同じ結果の形になるかを確認します。書き込みを伴う操作は、専用の権限を持つユーザーやトークンを分け、対象と上限、承認者、監査ログを決めてから段階的に広げてください。
まとめ|「定義はチームで、判定はスクリプト、下書きはAI、登録は人」
HubSpot CRM APIとSalesforce(sf CLI・SOQL)をClaude Codeにつなぐと、停滞商談の洗い出し、週次サマリー、重複の候補出し、活動記録の下書き、データ品質チェックといった「CRMを生きたデータに保つ」作業を、毎週の仕組みとして回せます。成功のポイントは、定義を文章で合意し、判定はスクリプトで決定的に行い、Claude Codeには整理と下書きを任せ、CRMへの登録は人の承認と監査ログを通すことです。まずはSandboxや検証用アカウントでfetch_deals.pyとstale_deals.py、またはsf data queryを動かし、対話で週次サマリーを1本作ってみてください。EC・決済データとつなぐ方法はClaude Code×Shopify・Stripe API、シリーズ全体の地図はAIエージェントで開発してみよう|API連携編をご覧ください。
業務自動化のスキルを、キャリアと収入につなげる
APIとAIエージェントで業務を自動化できる人材は、社内でも社外でも求められています。次の一歩に役立つ記事です。
- AIで独立するには?未経験から案件を取る6ステップ【2026】:自動化・開発スキルを案件にする。フリーランスエージェントの選び方も
- 仕事用の外付けモニターおすすめ3選【2026】:エディタ・ターミナル・ブラウザを並べると作業効率が段違い