マルチテナント設計でデータ漏洩を防ぐ:分離レベルとtenant_idの貫通
テナント分離は行/スキーマ/DBのどれを選ぶか、tenant_idをどう全層に通すか、Row Level Securityで最後に止める設計まで。SaaSの顧客データ漏れを防ぐ実装を僕の失敗込みでまとめました。
「とりあえずSaaSの形にしといて」
そう頼んで一晩で出てきた管理画面は、よくできていました。ログインも、案件一覧も、請求も動く。デモは完璧でした。
問題は翌週に起きました。テスト用にA社とB社を登録して、A社でログインしたまま案件のURLの末尾を1だけ増やしたら、B社の案件がそのまま開いたんです。請求金額まで丸見えでした。
背筋が凍りました。コードを見たら、案件を引くクエリに where tenantId が抜けている箇所が一つだけあった。たった一つです。でもSaaSでは、その一つが会社を潰します。
賢いAIに速く作らせるほど、こういう「境界の抜け」は増えます。理由は単純で、「動くコード」と「テナントが混ざらないコード」はまったく別の品質だからです。今日は、その境界をどう設計して、どこで最後に止めるかを書きます。
この記事の要点
- テナント分離は行レベル/スキーマレベル/DBレベルの3つ。多くのSaaSは行レベル(共有DB・共有スキーマ)から始めるのが現実的。
tenant_idはサーバー側で確定させる。URLやヘッダーから来た値は「候補」にすぎず、必ず所属確認を通す。- アプリの
where tenant_idは漏れる前提で、PostgreSQLのRow Level Security(RLS)を最後の防波堤にする。 - テナント識別はサブドメイン/カスタムドメイン/パスから解決し、
x-tenant-idのような自己申告ヘッダーは信用しない。 - 正常系テストでは足りない。A社からB社を読みに行く攻撃テストを先に書くと漏れが一発で見つかる。
マルチテナントSaaSとは、1つのアプリで複数の顧客組織(テナント)を扱う作り方です。SaaS全体の土台づくりはSaaSの雛形は認証・課金・テナント・ダッシュボードだけでいい、ロールごとの権限設計はRBAC実装で横移動を防ぐにまとめました。この記事はその中でも「テナントを混ぜない設計」だけに絞ります。
分離レベルを最初に選ぶ(行/スキーマ/DB)
設計で最初に決めるのは、テナントをどの粒度で分けるかです。選択肢は大きく3つ。ここを後から変えるのは引っ越しレベルの大工事なので、最初に腰を据えて選びます。
| 分離レベル | 中身 | 向いている場面 | 強み | つらい所 |
|---|---|---|---|---|
| 行レベル(共有DB・共有スキーマ) | 全テナントが同じ表を共有し、各行に tenant_id | 初期SaaS、顧客数が多いB2Bツール | 運用が軽い、横断分析が楽、マイグレーションが1回 | tenant_id の抜けが即漏洩。RLS必須 |
| スキーマレベル(共有DB・テナント別スキーマ) | テナントごとにスキーマを分け、同じ表を複製 | 大口顧客だけ分けたい業務SaaS | 権限とバックアップを少し分けられる | スキーマが増えると運用とマイグレーションが重い |
| DBレベル(テナント別DB) | テナントごとに物理DBを分ける | 金融・医療、契約で物理分離を要求される案件 | 鍵・バックアップ・停止範囲を完全に分けられる | コスト高、デプロイ・接続管理が複雑 |
判断の軸は「顧客が100社、1000社に増えたとき、その運用に耐えるか」です。多くの小中規模SaaSは行レベルから始めるのが正解です。スキーマやDBを分けると、マイグレーション1回が数百回のループになり、Claude Codeに「全テナントに同じ変更を」と頼むほど取りこぼしが怖くなります。
ただし、特定の顧客だけ「うちのデータは他社と同じDBに置かないでほしい」と契約で求めてくるケースはあります。そのときのために、行レベルで作りつつ、後から特定テナントだけ別DBへ逃がせる設計にしておくと安全です。具体的には、接続先を tenant_id から引ける一枚の関数にまとめておき、ふだんは共有DBを返し、特別な顧客だけ別の接続文字列を返す、という逃げ道を残します。
実際に僕が見てきた構成を3つ。1つ目はB2BのCRM。案件・会社・担当者・メモが全部テナント配下にあって、営業は自社の行しか見えない。これは行レベルがぴったりです。2つ目は制作会社向けのポータルで、代理店スタッフは複数テナントに所属するけれど、顧客担当者は自社だけ。これも行レベルで、所属(membership)を多対多にして解きます。3つ目は医療系で、1施設ごとにDBを分ける契約。これはDBレベルでした。同じ「マルチテナント」でも、答えは案件で変わります。
tenant_idは信頼できる場所から流す
漏洩の半分は「どこから tenant_id を取ったか」で決まります。やってはいけないのは、URLのクエリやクライアントが送ってくるヘッダーを、そのまま信じることです。
// ダメな例:自己申告を信じる
GET /api/projects?tenantId=acme ← 末尾を beta に書き換えれば他社が見える
x-tenant-id: acme ← ヘッダーは誰でも詐称できる
安全な順序はこうです。①ホスト名やパスから「候補テナント」を割り出す → ②サーバー側のセッションで「このユーザーは本当にそのテナントに所属しているか」を確認する → ③確認できた tenantId だけをアプリの内部に渡す。外から来た値は最後まで「候補」扱いで、所属確認を通った瞬間にはじめて本物になります。
セッションには「誰か(userId)」だけを持たせて、テナント所属はDBで毎回引く方が、レビューで追いやすいです。セッションに権限や顧客データを詰め込むと、権限を剥奪したのにセッションが古くて見えてしまう、という事故が起きます。
下のコードがその関門です。ポイントは一つだけ覚えてください。x-tenant-id を一切読んでいないこと。ホストからテナントを引き、membershipで所属を確認し、通った時だけ TenantContext を返します。
// src/lib/tenant-context.ts
import { z } from "zod";
const hostSchema = z.string().min(1).max(255);
export type SessionUser = { id: string; email: string };
export type TenantContext = {
tenantId: string;
userId: string;
role: "owner" | "admin" | "member" | "viewer";
requestId: string;
};
type TenantRecord = { id: string; subdomain: string | null; custom_domain: string | null };
type MembershipRecord = { tenant_id: string; user_id: string; role: TenantContext["role"] };
export async function resolveTenantContext(input: {
request: Request;
sessionUser: SessionUser | null;
requestId: string;
// ホスト名からテナントを引く(サブドメイン or カスタムドメイン)
findTenantByHost: (host: string) => Promise<TenantRecord | null>;
// そのユーザーがそのテナントに所属しているかを確認する
findMembership: (tenantId: string, userId: string) => Promise<MembershipRecord | null>;
}): Promise<TenantContext> {
if (!input.sessionUser) {
throw new Response("Unauthorized", { status: 401 });
}
// ① ホストから候補テナントを解決(クライアント送信値は使わない)
const host = hostSchema.parse(new URL(input.request.url).host.toLowerCase());
const tenant = await input.findTenantByHost(host);
if (!tenant) {
throw new Response("Tenant not found", { status: 404 });
}
// ② 所属確認。ここを通らない限り tenantId は確定しない
const membership = await input.findMembership(tenant.id, input.sessionUser.id);
if (!membership) {
throw new Response("Forbidden for this tenant", { status: 403 });
}
// ③ 確認済みの値だけをアプリ内部へ渡す
return {
tenantId: tenant.id,
userId: input.sessionUser.id,
role: membership.role,
requestId: input.requestId,
};
}
社内APIやバックグラウンドジョブでも同じ姿勢を貫きます。ジョブのpayloadに入っている tenantId も「候補」です。投入時に検証済みの値を入れ、ワーカー側でも署名やAPIキーで裏取りする。tenant_id を「外から来たら必ず疑う」と決めておくだけで、設計のブレが減ります。
セッションへのユーザーID追加は、Auth.jsのExtending the Sessionが公式の手順を示しています。認証そのものの組み方はClaude Codeで認証を実装する方法も合わせてどうぞ。
テナント識別:サブドメイン/カスタムドメイン/パス
「候補テナント」をどこから割り出すか。実務でよく使うのは次の3つです。それぞれ一長一短があります。
- サブドメイン(
acme.example.com): 一番よく使う形。テナントごとにブランド感が出て、Cookieも分離しやすい。ワイルドカードDNSと証明書(*.example.com)を用意します。 - カスタムドメイン(
app.acme-corp.jp): 大口顧客が「自社ドメインで使いたい」と言うパターン。ドメインとテナントの対応表をDBに持ち、証明書を顧客ごとに発行します。実装は重いですが、エンタープライズ商談で効きます。 - パス(
example.com/t/acme): DNSや証明書をいじれない初期段階で手軽。ただしCookieのスコープが全テナント共通になりやすく、取り違えのリスクが上がるので、本番では避けたい形です。
どの方式でも共通の鉄則は、識別の結果を「候補」として扱い、所属確認を必ず挟むこと。サブドメインを acme から beta に書き換えただけでログインが通るなら、それは識別を信頼しすぎています。前章の findTenantByHost → findMembership の二段構えが、ここで効いてきます。
Row Level Securityを最後の防波堤にする
ここが今日の核心です。アプリの where tenant_id = ... は必要ですが、人間(とAI)は必ずどこかで書き忘れます。冒頭の事故がまさにそれでした。だから、アプリが間違えてもDBが最後に止める層を入れます。それがPostgreSQLのRow Level Security(RLS)です。RLSは「行ごとにアクセスの可否をDB側で判定する仕組み」で、ポリシーに合わない行はそもそも見えなくなります。
仕組みはこうです。リクエストごとに「今はこのテナントの処理です」という印(app.tenant_id)を接続にセットし、各表のポリシーで「印と一致する行だけ見せる」と宣言します。アプリ側でクエリに条件を書き忘れても、DBが勝手に他テナントの行を隠してくれます。
一つ落とし穴があります。テーブルの所有者ロールや BYPASSRLS 属性を持つロールは、RLSをすり抜けます。だからアプリの接続ロールを表の所有者にしない、守りたい表では FORCE ROW LEVEL SECURITY を付けて所有者にも適用する、という運用を合わせます。
-- db/migrations/20260602_multi_tenant_rls.sql
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE tenants (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
slug text NOT NULL UNIQUE,
name text NOT NULL,
plan text NOT NULL DEFAULT 'starter',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE app_users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE tenant_memberships (
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES app_users(id) ON DELETE CASCADE,
role text NOT NULL CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (tenant_id, user_id)
);
CREATE TABLE projects (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name text NOT NULL,
status text NOT NULL DEFAULT 'active',
created_by uuid NOT NULL REFERENCES app_users(id),
created_at timestamptz NOT NULL DEFAULT now()
);
-- tenant_id を先頭に置いた複合インデックスで絞り込みを速くする
CREATE INDEX projects_tenant_id_id_idx ON projects (tenant_id, id);
CREATE INDEX tenant_memberships_user_id_tenant_id_idx ON tenant_memberships (user_id, tenant_id);
-- RLSを有効化し、所有者にも強制する
ALTER TABLE tenant_memberships ENABLE ROW LEVEL SECURITY;
ALTER TABLE tenant_memberships FORCE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects FORCE ROW LEVEL SECURITY;
-- 印(app.tenant_id)と一致する行だけ読み書きできる
CREATE POLICY projects_isolation
ON projects
FOR ALL
USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid)
WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid);
CREATE POLICY tenant_memberships_isolation
ON tenant_memberships
FOR ALL
USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid)
WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid);
USING は読める行の条件、WITH CHECK は書ける行の条件です。両方を入れることで、A社のコンテキストでB社の行を読むことも、A社のふりをしてB社の行を挿入することも防げます。nullif(..., '') を挟んでいるのは、印が未設定(空文字)のときに「全件見える」のではなく「0件」になるようにする保険です。
仕様は必ず公式で確認してください。RLSの全体像はPostgreSQLのRow Security Policies、ポリシー構文はCREATE POLICY、セッション変数はcurrent_settingとset_configが一次情報です。
印は接続プールで漏らさない(set_configの罠)
RLSで一番事故るのが、この「印」の付け方です。接続プール(pg-poolなど)を使うと、接続は使い回されます。SET app.tenant_id = 'acme' のようにセッションに残る形で印を付けると、次にその接続を借りた別テナントの処理に、前の印が残ったままになります。これは即、横断漏洩です。
正解は、トランザクション内で set_config(..., true) を使うこと。第3引数の true は「このトランザクションが終わったら印を消す」という意味です。処理が終われば自動で消えるので、次の利用者に持ち越されません。下のラッパーを必ず通してDBを触る、というルールにすると安全です。
// src/db/tenant-db.ts
import { Pool, PoolClient } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// tenant_id の形を最初に検証(おかしな値はDBに渡さない)
const uuidPattern =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
export async function withTenant<T>(
tenantId: string,
work: (client: PoolClient) => Promise<T>,
): Promise<T> {
if (!uuidPattern.test(tenantId)) {
throw new Error("Invalid tenant id");
}
const client = await pool.connect();
try {
await client.query("BEGIN");
// 第3引数 true = トランザクション終了で印が自動的に消える
await client.query("SELECT set_config('app.tenant_id', $1, true)", [tenantId]);
const result = await work(client);
await client.query("COMMIT");
return result;
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
// 使う側:SQLに where tenant_id を書いていないのに、自社の行しか返らない
export async function listProjects(tenantId: string) {
return withTenant(tenantId, async (db) => {
const result = await db.query(
"SELECT id, name, status, created_at FROM projects ORDER BY created_at DESC",
);
return result.rows;
});
}
listProjects のSQLに WHERE tenant_id = $1 が無いのは、わざとです。RLSが効いていれば、DBが現在のテナントの行だけを返します。性能のためにアプリ側にも条件を足すのは構いませんが、足してもRLSは外さない。二重に守る、が鉄則です。
tenant_idはDB以外も貫通させる
DBだけ守って安心していると、別の経路から漏れます。tenant_id は「アプリの全層を貫く一本の串」として扱います。串が通っていない場所が、次の事故現場です。
- バックグラウンドジョブ: 一番危ない。キューに
projectIdだけ入れて、ワーカーで全体検索すると他社の行を引けてしまう。投入時に検証済みtenantIdを入れ、ワーカーでもwithTenantで印をセットします。 - ファイルストレージ: DBが守れても、S3キーが
uploads/project-123/file.pdfだと推測でアクセスされかねません。tenant_idをパスに含め、署名付きURLで配ります。 - 検索インデックス: Algolia・Meilisearch・OpenSearchはDBの外。絞り込みキーに
tenant_idを必ず入れ、検索時に強制フィルタをかけます。 - ログと監査: ログは第二のDBになりがち。
tenant_idは残しつつ、本文・Cookie・Authorization・APIキー・プロンプト全文・請求先は落とします。OWASPのLogging Cheat Sheetが定番の指針です。 - キャッシュ: キャッシュキーに
tenant_idを入れ忘れると、A社の結果がB社に返ります。キーの先頭に必ずテナントを付けます。
ジョブの例だけコードで示します。payloadの tenantId を信じきらず、引いた行がそのテナントに属するかを withTenant 経由で確認しています。
// src/jobs/send-project-digest.ts
import { withTenant } from "../db/tenant-db";
type ProjectDigestJob = { tenantId: string; projectId: string; requestedBy: string };
export async function handleProjectDigestJob(job: ProjectDigestJob) {
// ワーカーでも印をセット。RLSが効くので別テナントの行は引けない
return withTenant(job.tenantId, async (db) => {
const project = await db.query("SELECT id, name FROM projects WHERE id = $1", [
job.projectId,
]);
// RLS下で0件なら「このテナントには見えない行」を意味する
if (project.rowCount !== 1) {
throw new Error("Project not visible for tenant");
}
await db.query(
"INSERT INTO audit_events (tenant_id, actor_user_id, event_name) VALUES ($1, $2, $3)",
[job.tenantId, job.requestedBy, "project_digest_sent"],
);
});
}
漏洩を狙うテストを先に書く
ここで強く言いたいのが、正常系テストだけでは漏洩は絶対に見つからないということです。「A社で案件が見える」テストは100個書いても、「A社からB社が見えてしまう」バグを捕まえません。攻撃する側の視点でテストを書きます。
書くべき観点はこの5つ。①A社のセッションでB社のIDをURLに入れたら404か403になる、②印(app.tenant_id)をセットせずに投げたクエリは0件かエラーになる、③ジョブに tenantId 無しのpayloadを渡したら拒否される、④プラン上限を超えたらDB書き込み前に止まる、⑤ログにtokenやprompt全文が出ない。
下はRLSの最小スモークテストです。A社の印をセットした状態で projects を全件取り、本当に1件(自社分)だけかを検証します。psqlだけで動くので、CIに組み込めます。
-- db/tests/rls-smoke-test.sql
\set ON_ERROR_STOP on
BEGIN;
INSERT INTO tenants (id, slug, name) VALUES
('00000000-0000-4000-8000-000000000001', 'alpha', 'Alpha Inc'),
('00000000-0000-4000-8000-000000000002', 'beta', 'Beta Inc');
INSERT INTO app_users (id, email)
VALUES ('10000000-0000-4000-8000-000000000001', '[email protected]');
INSERT INTO projects (id, tenant_id, name, created_by) VALUES
('20000000-0000-4000-8000-000000000001', '00000000-0000-4000-8000-000000000001', 'Alpha project', '10000000-0000-4000-8000-000000000001'),
('20000000-0000-4000-8000-000000000002', '00000000-0000-4000-8000-000000000002', 'Beta project', '10000000-0000-4000-8000-000000000001');
-- A社(alpha)の印をセット
SELECT set_config('app.tenant_id', '00000000-0000-4000-8000-000000000001', true);
DO $$
DECLARE visible_count integer;
BEGIN
SELECT count(*) INTO visible_count FROM projects;
IF visible_count <> 1 THEN
RAISE EXCEPTION 'RLS failed: expected 1 visible project, got %', visible_count;
END IF;
END $$;
ROLLBACK;
Claude Codeにテストを頼むときは、観点を文章で固定して渡すと精度が上がります。「正常系を追加して」ではなく、攻撃ケースを名指しするのがコツです。
次の観点でマルチテナント漏洩テストを追加してください。
1. Tenant AのセッションでTenant BのprojectIdを指定しても404または403になる。
2. RLSのapp.tenant_idをセットしないDBクエリは0件またはエラーになる。
3. バックグラウンドジョブはtenantIdなしのpayloadを拒否する。
4. プラン上限超過時はDB書き込み前に402を返す。
5. ログにauthorization、cookie、prompt、apiKeyが出ない。
失敗する実装を先に説明し、修正後に実行コマンドと結果を示してください。
既存SaaSを後からマルチテナント化する
ゼロから作るより、後付けの方がずっと事故ります。最大の罠は「nullableな tenant_id を足して、少しずつ直す」やり方です。移行期間として一時的に許す場合はあっても、公開前には NOT NULL・外部キー・インデックス・RLSまで必ず完了させます。「あとで直す」は本番で必ず破綻します。
段階を切って依頼すると、Claude Codeの取りこぼしが減ります。スキーマ変更そのものの進め方はデータベースマイグレーションも参考にしてください。
既存テーブルをマルチテナント化する移行計画を作ってください。
Phase 1: tenant_id列を追加し、既存データの対応表を作る。
Phase 2: backfill SQLを作り、NULL件数を検証する。
Phase 3: NOT NULL、外部キー、tenant_id付き一意制約、インデックスを追加する。
Phase 4: RLSを有効化し、FORCE ROW LEVEL SECURITYの対象を提案する。
Phase 5: API、ジョブ、ログ、検索インデックスの漏れをテストする。
各Phaseにロールバック条件と検証SQLを付けてください。
依頼文そのものにも境界を入れておくと安心です。「tenant_idを自動で補って」「今動けばよい」「管理者は全部見えるでよい」という指示は、そのまま漏洩への近道になります。禁止事項(RLSを外さない、x-tenant-idを信じない、機密をログに出さない)を CLAUDE.md に固定し、毎回のレビューで「tenant_id が全層に伝播しているか」「RLSが効いているか」を確認する運用にします。Claude Code側の権限やMCP接続先の固定は、AnthropicのClaude Code Securityが一次情報です。
よくある質問
Q. 行レベルとDBレベル、結局どっちで始めるべき? 迷ったら行レベル(共有DB・共有スキーマ)です。運用が軽く、マイグレーションが1回で済みます。物理分離を契約で求められる顧客が現れたときに、その顧客だけDBレベルへ逃がせる設計にしておけば十分です。最初から全社をDBレベルにすると、コストとデプロイの複雑さで身動きが取れなくなります。
Q. アプリ側で where tenant_id を必ず書けば、RLSは要らない?
要ります。人間もAIも、必ずどこかで書き忘れます。冒頭の僕の事故がその実例です。RLSは「書き忘れても止まる」最後の層です。性能のためにアプリ側に条件を足すのは良いですが、RLSは外さないでください。
Q. RLSを入れると遅くなりませんか?
ポリシーの条件(tenant_id = ...)が効くように、tenant_id を先頭にした複合インデックスを張れば、実用上の負担は小さく収まります。むしろ怖いのは、インデックスが無いまま全テナントの巨大な表をスキャンすることです。インデックス設計はRLSとセットで考えます。
Q. テナント識別はサブドメインとパス、どっちがいい? 本番ではサブドメイン(またはカスタムドメイン)を勧めます。パス方式はCookieのスコープが全テナント共通になりやすく、取り違えのリスクが上がります。初期の検証ではパスでも構いませんが、いずれにせよ識別結果は「候補」として所属確認を必ず挟みます。
Q. 管理者やサポート担当は全テナントを見ていい? RLSを素通しにするのは危険です。サポートの閲覧は、監査ログ付きの明示的な「成り代わり(impersonation)」として実装します。誰が・いつ・どのテナントを見たかを必ず記録し、ふだんの権限とは分けます。
この記事で紹介した内容を実際に試した結果
冒頭の「URL末尾を1増やしたら他社が見えた」事故のあと、僕は方針を変えました。「クエリに where tenant_id を漏れなく書く」を頑張るのをやめて、書き忘れてもDBが止める設計に倒したんです。
小さなCRMサンプルで withTenant とRLSを入れ直して、最初に検証したのは正常系ではなく「A社からB社を読みに行くテスト」でした。そこで一発で見つかったのが、画面ではなく日次メールのジョブです。UIは tenantId を渡していたのに、キューのpayloadが projectId だけで、ワーカーが他テナントの行を引ける形になっていました。ジョブにも withTenant を強制し、印が未設定なら0件になるSQLテストを足したら、同じ漏れを再現できなくなりました。
結局のところ、テナント分離は「賢く書く」より「間違えても漏れない層を先に作る」のが速い、というのが今の実感です。手を動かして固めたい人は無料チートシートから、チーム導入で境界条件をレビューに乗せたい人はClaude Code研修・導入相談もどうぞ。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。