ハーネスエンジニアリングとは?Codex事例と安全な実装手順
ハーネスエンジニアリングを初心者向けに解説。OpenAIのCodex事例、役割分担、安全なファイル操作、テストまで実装します。
「リポジトリを整理して」とAIに頼んだら、関係ない設定まで変更された。テストを任せたら、「成功しました」という報告だけで実際には実行されていなかった。AIエージェントを使い始めると、こうした手戻りが起きます。
原因は、プロンプトの言い回しだけではありません。AIが読める情報、使える道具、止める条件、合格判定が曖昧なままだからです。この外側の仕組みを設計する仕事がハーネスエンジニアリングです。ハーネスは、エージェントが作業するための「足場」と考えると分かりやすいでしょう。
この記事では、話題の背景をOpenAIの実例から確認したうえで、Claude Codeなどにも応用できる最小構成を作ります。掲載コードはファイル境界のテストを実行し、どこまで検証したかを最後に明記します。
この記事の要点
- ハーネスは単一のプログラムではなく、ルール・道具・権限・テスト・ログ・人の承認を組み合わせた作業環境です。
- OpenAIはCodex中心の開発で、リポジトリ内の知識、構造テスト、観測可能性をエージェントから読める形にしました。
- AIには調査・下書き・反復作業を任せ、削除・本番変更・送信・課金の判断は人が持ちます。
- パス文字列の前方一致だけでは不十分です。シンボリックリンクと上書きを考慮し、OSの隔離も併用します。
- 「テスト済み」という文章ではなく、コマンドの終了コードと検証範囲を残します。
ハーネスエンジニアリングとは何か
プロンプトは「今回やってほしいこと」を伝えます。ハーネスは、その指示を実行する環境を決めます。たとえば、次の6点です。
| 要素 | 決める内容 | 最小例 |
|---|---|---|
| コンテキスト | 何を読ませるか | AGENTS.md、対象フォルダ、仕様書 |
| ツール | 何を実行できるか | 読み取り、テスト、下書き作成 |
| 権限 | どこで止めるか | 削除と外部送信は人の承認 |
| 検証 | 何をもって合格とするか | npm testの終了コードが0 |
| 観測 | 失敗をどう追うか | 実行コマンド、差分、エラーログ |
| 回復 | 失敗後にどう戻すか | 小さいコミット、dry-run、ロールバック |
この関係を図にすると、AIモデルは中央の一部にすぎません。
人が決める目的・承認条件
↓
リポジトリ内のルール → AIエージェント → 許可されたツール
↑ ↓
仕様・過去判断 テスト・ログ・差分
└──── 不合格なら修正、危険なら人へ ────┘
モデルを替えるだけでは、リポジトリ固有の事情は伝わりません。逆に、必要な情報と機械的な合格条件が揃えば、同じモデルでも仕事は安定します。
なぜ2026年に注目されたのか:OpenAIのCodex事例
「ハーネスエンジニアリング」という言葉が広く注目されたきっかけの一つが、OpenAIが2026年2月11日に公開したCodex中心の開発事例です。
OpenAIの報告では、3人のエンジニアがCodexを使い、約5か月でおよそ1,500件のPRを作成しました。重要なのは数字そのものより、人がコードを直接書かない前提で、仕事の環境を作り直した点です。
- 設計判断や計画をリポジトリ内の文書にする
- UI、ログ、メトリクスをエージェントから確認できるようにする
- 依存方向や命名規則をカスタムlintと構造テストで強制する
- 失敗したら「もう一度頑張れ」ではなく、足りない道具やルールを追加する
- 古くなった文書を定期的に見つけて修正する
これは「長いプロンプトを書けば自動化できる」という話ではありません。エージェントから見えない社内Wikiや口頭ルールは、作業中には存在しないのと同じです。必要な知識を小さく、検索可能で、更新履歴が残る形にする。さらに、守るべき条件を文章だけでなくテストにする。この2点が中心です。
Claude Codeでも考え方は同じです。Claude Agent SDKのhooksでは、ツール実行前に操作を拒否したり、入力を修正したり、監査ログを残したりできます。製品名は違っても、エージェントの外側で境界とフィードバックを作る点は共通しています。
Claude Codeに任せる範囲と、人が判断する範囲
最初から全自動にしないでください。戻せる作業は自動、戻せない作業は承認制に分けます。
| AIに任せやすい | 条件付きで任せる | 人が最終判断する |
|---|---|---|
| ファイル検索 | 既存ファイルの編集 | 本番データの削除 |
| テスト実行 | 新しい依存関係の追加 | 顧客へのメール送信 |
| 差分要約 | ステージングへの公開 | 課金・契約変更 |
| 下書き作成 | Gitへのpush | 個人情報を含む処理 |
判断基準は「失敗したときに元へ戻せるか」「外部の人や金銭に影響するか」です。読み取りと一時ファイル作成から始め、成功パターンが溜まった操作だけ自動化します。
動く確認コードで実装する最小ハーネス
ここでは、確認コードを先に用意し、AIに次の2つのファイル操作だけを渡します。
sandbox内のテキストを読むsandbox内に新しいテキストを作る
削除、上書き、シェル実行、ネットワーク送信は渡しません。まず作業フォルダを準備します。Node.js 22で確認し、SDKは検証時点のバージョンへ固定します。
mkdir harness-demo
cd harness-demo
npm init -y
npm install @anthropic-ai/[email protected]
mkdir sandbox
echo "# meeting notes" > sandbox/note.md
policy.jsonに作業範囲と反復回数を書きます。
{
"workspace": "./sandbox",
"maxSteps": 6,
"maxToolResultChars": 4000
}
1. ファイル境界をコードで守る
safe-files.mjsを作ります。単純なstartsWith(root)だけでは、sandbox-oldのような似た名前や、外部を指すシンボリックリンクを見逃す場合があります。読み取り時は実体パスを確認し、書き込みは新規ファイルだけに限定します。
import { open, readFile, realpath } from "node:fs/promises";
import path from "node:path";
function assertInside(root, candidate) {
if (candidate !== root && !candidate.startsWith(root + path.sep)) {
throw new Error(`outside workspace: ${candidate}`);
}
}
export async function createFileGate(workspace) {
const root = await realpath(path.resolve(workspace));
async function readText(relativePath) {
const requested = path.resolve(root, relativePath);
assertInside(root, requested);
const actual = await realpath(requested);
assertInside(root, actual);
return readFile(actual, "utf8");
}
async function createText(relativePath, content) {
const requested = path.resolve(root, relativePath);
assertInside(root, requested);
const actualParent = await realpath(path.dirname(requested));
assertInside(root, actualParent);
let handle;
try {
handle = await open(requested, "wx", 0o600);
await handle.writeFile(content, "utf8");
} catch (error) {
if (error.code === "EEXIST") {
throw new Error(`refusing to overwrite: ${relativePath}`);
}
throw error;
} finally {
await handle?.close();
}
return "created";
}
return { readText, createText };
}
このコードはアプリ側の門番です。完全な隔離ではありません。強い境界が必要なら、コンテナ、仮想マシン、OS権限、Claude Codeのsandboxなどを併用します。アプリのチェックだけでOS管理者権限まで防げる、と考えないでください。
2. AIに渡すツールを2つに絞る
次にagent.mjsを作ります。モデル名はコードへ固定せず、利用できるモデルをANTHROPIC_MODELで指定します。
import Anthropic from "@anthropic-ai/sdk";
import { readFile } from "node:fs/promises";
import { createFileGate } from "./safe-files.mjs";
const model = process.env.ANTHROPIC_MODEL;
if (!model) throw new Error("Set ANTHROPIC_MODEL to a model available to your account.");
const policy = JSON.parse(await readFile("./policy.json", "utf8"));
const gate = await createFileGate(policy.workspace);
const client = new Anthropic();
const tools = [
{
name: "read_file",
description: "Read a UTF-8 text file inside the workspace",
input_schema: {
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
additionalProperties: false
}
},
{
name: "create_file",
description: "Create a new UTF-8 file; existing files cannot be overwritten",
input_schema: {
type: "object",
properties: {
path: { type: "string" },
content: { type: "string" }
},
required: ["path", "content"],
additionalProperties: false
}
}
];
async function runTool(name, input) {
if (name === "read_file") return gate.readText(input.path);
if (name === "create_file") return gate.createText(input.path, input.content);
throw new Error(`unknown tool: ${name}`);
}
const prompt = process.argv.slice(2).join(" ") ||
"Read note.md and create summary.md with a three-line summary.";
const messages = [{ role: "user", content: prompt }];
for (let step = 0; step < policy.maxSteps; step += 1) {
const response = await client.messages.create({
model,
max_tokens: 1200,
system: "Use only the supplied tools. Never claim a file was created unless the tool succeeded.",
tools,
messages
});
messages.push({ role: "assistant", content: response.content });
const calls = response.content.filter((block) => block.type === "tool_use");
if (calls.length === 0) {
console.log(response.content.find((block) => block.type === "text")?.text ?? "done");
process.exit(0);
}
const results = [];
for (const call of calls) {
try {
const value = await runTool(call.name, call.input);
results.push({
type: "tool_result",
tool_use_id: call.id,
content: String(value).slice(0, policy.maxToolResultChars)
});
} catch (error) {
results.push({
type: "tool_result",
tool_use_id: call.id,
is_error: true,
content: error.message
});
}
}
messages.push({ role: "user", content: results });
}
throw new Error(`step limit exceeded: ${policy.maxSteps}`);
3. AIを呼ぶ前に門番をテストする
API料金を使わなくても、重要な境界はローカルで確認できます。safe-files.test.mjsを作ります。
import assert from "node:assert/strict";
import test from "node:test";
import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { createFileGate } from "./safe-files.mjs";
test("file gate blocks traversal, overwrite, and outside symlinks", async () => {
const base = await mkdtemp(path.join(tmpdir(), "harness-test-"));
const root = path.join(base, "sandbox");
const outside = path.join(base, "outside.txt");
try {
await mkdir(root);
await writeFile(path.join(root, "note.md"), "hello", "utf8");
await writeFile(outside, "secret", "utf8");
const gate = await createFileGate(root);
assert.equal(await gate.readText("note.md"), "hello");
await assert.rejects(() => gate.readText("../outside.txt"), /outside workspace/);
await assert.rejects(() => gate.createText("note.md", "replace"), /refusing to overwrite/);
try {
await symlink(outside, path.join(root, "outside-link.txt"), "file");
await assert.rejects(() => gate.readText("outside-link.txt"), /outside workspace/);
} catch (error) {
if (error.code !== "EPERM") throw error;
}
assert.equal(await gate.createText("summary.md", "safe"), "created");
} finally {
await rm(base, { recursive: true, force: true });
}
});
実行します。
node --test safe-files.test.mjs
node --check agent.mjs
テストが通ったら、APIキーとモデル名を環境変数へ設定してnode agent.mjsを実行します。秘密情報をソースコードやpolicy.jsonへ書かないでください。
3つのUse case
コード変更だけでなく、記事やメールの下書きにも同じ「入力・出力・人の確認」の枠を使えます。次の3例は、どこまで自動化するかを決めるための最小設計です。
Use case 1:開発チームのPR実装と検証
- 入力: 対象Issue、関連ディレクトリ、失敗を再現するテストコマンド
- 出力: 修正コード、テスト結果、変更したファイルの差分要約
- 人の確認: 本番デプロイ、データベース移行、依存関係の追加
完了条件は「コードを書いた」ではありません。「再現テストが失敗する、修正後に成功する、差分を説明できる」の3点です。
Use case 2:メディア運営の記事公開前チェック
- 入力: 記事ファイル、既存タイトル一覧、品質チェックリスト
- 出力: 文字数、コード構文、リンク、モバイル表示の検査結果
- 人の確認: 事実関係、独自の経験、最終的な公開判断
不合格なら公開せず、具体的なエラーを執筆エージェントへ返します。本サイトでも記事ごとの検証スクリプトをCIで実行し、検証を通らない変更はデプロイしない構成にしています。
Use case 3:問い合わせ対応の分類と返信下書き
- 入力: 個人情報を伏せた問い合わせ本文、返信テンプレート、対応ルール
- 出力: 問い合わせ分類、返信案、参照した根拠
- 人の確認: 顧客台帳への登録、宛先、文面、実際のメール送信
ログには問い合わせ本文をそのまま残さず、処理IDと判定結果だけを記録します。
導入前後のROIをどう見るか
効果は「生成した文字数」ではなく、人の確認時間と手戻りで測ります。たとえば、1件20分の確認を週15件行っているなら週5時間です。初期構築6時間、週次保守1時間まで減らせれば、単純計算では約1.5週間で構築時間を回収します。
これは説明用の試算であり、保証値ではありません。導入前に次の4つを2週間測り、導入後と比較してください。
- 1件あたりの人の作業時間
- 差し戻し率
- 本番前に検出できた不具合数
- 人の承認が必要になった回数
承認回数が多すぎるなら、低リスク操作の範囲を少し広げます。事故や差し戻しが増えたなら、自動化を広げるのではなく、足りない検証やルールを追加します。
Pitfall: よくある失敗と直し方
失敗の原因は、文章上の禁止と実行時の制限を混同することです。直し方は、危険な操作をツールから外し、コードとOS権限で拒否し、テストで拒否動作まで確認することです。
「フォルダ名を確認したから安全」と考える
文字列上はworkspace内でも、シンボリックリンクの実体が外部を指すことがあります。実体パスを検査し、上書きを禁止し、さらにOS側でも権限を分けます。
プロンプトに「危険操作禁止」とだけ書く
文章は判断材料であり、強制境界ではありません。禁止したい操作はツールとして渡さないか、実行前hookで拒否します。Claude Codeの具体的な設定は権限設定ガイドで確認できます。
AIの「テストしました」を合格証にする
テストコマンド、終了コード、対象範囲を記録します。UI変更ならスクリーンショットや操作結果も必要です。検証レシートの作り方も合わせて使ってください。
コンテキストを全部詰め込む
長い指示を毎回渡すと、重要な制約が埋もれます。入口は短くし、必要な仕様へリンクする構成にします。古い文書が残る場合は、更新日と検証状態を持たせます。
よくある質問
Claude Codeだけで使う考え方ですか?
いいえ。Codexなど別のAIエージェントでも、読ませる情報、使えるツール、承認条件、合格テストを外側に置く考え方は共通です。製品固有の設定と、共通の設計原則を分けてください。
最初からhookを作る必要がありますか?
最初は不要です。読み取り専用の仕事を一つ選び、入力ファイルと合格コマンドを決めるところから始めます。同じ拒否判断が繰り返された時点で、その判断をhookやテストへ移します。
このファイル境界だけで本番利用できますか?
できません。掲載コードはアプリケーション側の門番です。本番では、専用ユーザー、コンテナ、読み取り専用マウント、ネットワーク制限、秘密情報の分離など、OSや実行環境の境界も重ねます。
まとめ
ハーネスエンジニアリングは、AIへ長い指示を書く技術ではありません。必要な知識を読める場所へ置き、使える道具を絞り、危険操作を止め、結果をコマンドで判定し、失敗を次のルールへ戻す設計です。
最初の一歩は、仕事を一つ選び、「入力」「許可する操作」「合格コマンド」「人が承認する操作」を4行で書くことです。チームで実際のリポジトリへ権限・検証・レビューの足場を組み込みたい場合は、Claude Code研修・導入相談で現在の業務フローから整理できます。
実際に試した結果
2026年7月21日に、この記事のsafe-files.mjsとsafe-files.test.mjsを一時ディレクトリへ抽出し、Node.jsでテストしました。通常の読み取り、新規作成、../による範囲外アクセス、既存ファイルの上書き拒否を確認し、シンボリックリンクを作成できる環境では外部リンクの拒否も確認します。agent.mjsは構文チェックを行いました。
一方、ライブAPI呼び出しは検証範囲に含めていません。利用者ごとにモデル権限と料金が異なるためです。この区別を残すこと自体がハーネスの一部です。「コードを掲載した」「構文が通った」「外部APIまで動かした」を同じ意味にしないようにします。
まず今日やるなら、記事内の3つのコードブロックを同じフォルダへ置き、node --test safe-files.test.mjsを実行してください。終了コードが0になることを確認したら、policy.jsonの作業フォルダだけを自分の検証用リポジトリに合わせます。本番データや顧客ファイルでは試さないでください。
次に読む記事
Claude Code 軽量ハーネス:型チェック+テスト+確認だけ回す最小ループ
個人開発や小さなWebサイトに本格ハーネスは過剰。型チェック・テスト・公開確認の3つだけ回す軽量ハーネスを、コピペで動くシェルスクリプト付きで紹介します。
自律エージェントを安全に走らせる足場:Codexの暴走を止める7つのガード
Codexのような自律AIに作業を任せて事故らないために。サンドボックス、権限の最小化、出力の検証、破壊的操作のガード、秘密の保護を、コピペで動く設定と僕の失敗談で。
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。