結論

ハーネスとは、AIエージェントのうちLLM本体以外の仕組み全部です。道具の実行・許可・記憶・ループを受け持ちます。

この記事でわかること

  • 「ハーネス」7つの意味の見分け表と8つの部品
  • 102行のPythonで作る最小ハーネスと全出力
  • OpenCode・Clineが送る道具10個・25個の中身

2026年10月18日更新|OpenCode 1.19.3・Cline 3.0.73・自作で実測

モデルは「どの道具を使いたいか」を書くだけで、実際に読む・実行する・止めるのはハーネスです。

flowchart TD
    U[人の指示] --> H[ハーネス<br>道具・許可・記憶・ループ]
    H -->|会話と道具の一覧| M[LLM モデル]
    M -->|道具を使いたい| H
    H -->|許可されたら実行| T[道具<br>ファイル・シェル・Web]
    T -->|結果| H

ハーネスが受け持つ部品を先にまとめます。

部品 ハーネスがすること Claude Code での名前
道具(ツール) 道具の定義を渡し、呼ばれたら実行する Read・Edit・Bash など
許可 実行してよいかを決め、必要なら人に聞く 権限モード・allow/deny
ループ 結果をモデルに返し、終わるまで繰り返す エージェントループ
記憶(指示ファイル) 毎回読ませる決まりを会話に入れる CLAUDE.md・自動メモリー
コンテキスト管理 長い会話を要約し、大きな出力を削る 自動の圧縮・/compact
フック 道具の前後に決まった処理を必ず走らせる Hooks(PreToolUse など)
サブエージェント 別の文脈で仕事を任せ、要約だけ受け取る Agent ツール
戻す仕組み 変更前を控えて、戻せるようにする チェックポイント

確認環境:Ubuntu 24.04・Python 3.11.15(標準ライブラリだけで書いた最小ハーネスと、OpenAI 互換の擬似 LLM サーバー)・Node.js 22.22.2・OpenCode 1.19.3(npm の opencode-ai)・Cline CLI 3.0.73(npm の cline)。OpenCode と Cline は擬似サーバーにつなぎ、届いたリクエストの道具の数・大きさ・許可の動きを記録しました。Claude Code の部品は code.claude.com のドキュメントで、OpenCode・Cline の部品は opencode.ai と docs.cline.bot のドキュメントで、言葉の出どころは OpenAI・Anthropic・LangChain・martinfowler.com・Mitchell Hashimoto 氏の原文で確かめました(確認日 2026年10月18日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「ハーネスとは|AIエージェントの仕組みとハーネスエンジニアリング入門」

ハーネスとは:AIエージェントの「モデル以外の全部」

AI の分野でいうハーネス(harness)は、LLM(大規模言語モデル)の外側で、道具の実行・許可・記憶・ループなどを受け持つ仕組みのことです。 もとの英語は馬に付ける「馬具」で、力の強い馬を目的の方向へ働かせる道具一式を指します。

主な定義を原文から並べます。

出典 定義(原文)
LangChain「The Anatomy of an Agent Harness」(2026年3月10日) "A harness is every piece of code, configuration, and execution logic that isn't the model itself."
martinfowler.com(Birgitta Böckeler、2026年4月2日) "everything in an AI agent except the model itself"
Claude Code ドキュメント「How Claude Code works」 "Claude Code is the layer around the model that provides the tools and manages the context the model sees. This surrounding layer is what the term agentic harness refers to."

LangChain の記事は、この関係を 「Agent = Model + Harness」(エージェント=モデル+ハーネス) という式で表しています。Claude Code・OpenCode・Cline のような AI コーディングエージェントは、どれも「モデル」に「ハーネス」をかぶせた製品です。

関連記事「AIエージェントとは|仕組みとワークフローとの違い・作り方入門」で扱っています。

「ハーネス」の意味の見分け表

「ハーネス」は分野によって指すものがまったく違います。検索結果で混ざりやすいので、一緒に出てくる言葉で見分けます。

分野 「ハーネス」が指すもの 一緒に出てくる言葉
AI エージェント LLM の外側で道具・許可・記憶・ループを受け持つ仕組み エージェント・Claude Code・ハーネスエンジニアリング
AI の評価 評価ハーネス:ベンチマークをまとめて流して採点する仕組み 評価・ベンチマーク・スコア
ソフトウェアテスト テストハーネス:テスト対象を動かすスタブとドライバーの一式 テストハーネス・スタブ・ドライバー
電気・自動車 ワイヤーハーネス:電線を束ねて1つの部品にしたもの ワイヤーハーネス・コネクター・車載
高所作業 フルハーネス:体を支える墜落制止用器具 フルハーネス・安全帯・特別教育
ペット・馬 胴に付けるベルト、馬に付ける引き具 犬・リード・馬具
製品名 Harness 社の CI/CD などのサービス Harness CD・パイプライン

テストハーネスは、Wikipedia(英語版)で "a collection of stubs and drivers configured to assist with the testing of an application or component" と説明されています。「対象を外から動かし、結果を見る仕組み」という点で、AI エージェントのハーネスと考え方が近い言葉です。

モデルがすることと、ハーネスがすること

モデルは文章を返すだけで、ファイルを読んだりコマンドを動かしたりはしません。「この道具をこの引数で使いたい」という JSON を返すと、それを受け取ったハーネスが実行します。

場面 モデル(LLM)がすること ハーネスがすること
始まり 何もしない システムプロンプト・指示ファイル・道具の一覧を用意して送る
判断 文章で答えるか、道具を使うかを決める 何もしない(返事を待つ)
道具の呼び出し 道具の名前と引数を JSON で書く 許可を確かめ、実際に読む・書く・実行する
結果 結果を読んで次を決める 結果を会話に足して、もう一度送る
終わり 「終わった」と文章で返す 回数の上限・費用の上限で止める

この流れは OpenAI の function calling(tool use)の形そのものです。関連記事「Function Calling(tool use)の仕組みとPython実装」で扱っています。

ハーネスが受け持つ8つの部品

冒頭の早見表の部品を、「無いと何が起きるか」と合わせて見ます。

部品 何をするか 無いと起きること
道具(ツール) 読む・書く・実行する・調べる手段をモデルに渡す モデルは文章で答えるだけになる
許可 道具ごと・引数ごとに、通す/聞く/断るを決める 消す・送るなどの操作が確認なしで走る
ループ 道具の結果を返し、終わるまで繰り返す。上限を持つ 1回で終わるか、止まらなくなる
記憶(指示ファイル) AGENTS.md・CLAUDE.md などを毎回会話に入れる 規約を毎回言い直す必要がある
コンテキスト管理 前の出力を消す・要約する・大きな出力を削る 会話があふれ、前の指示を忘れる
フック 道具の前後にテスト・リンター・記録を必ず走らせる 守らせたい処理がモデルの判断任せになる
サブエージェント 調べ物などを別の文脈に任せ、要約だけ受け取る 本筋の会話が調べ物の出力で埋まる
戻す仕組み 変更前のファイルを控え、戻せるようにする 失敗した変更を手で戻すことになる

LangChain の記事は、ハーネスの部品として「システムプロンプト」「道具・スキル・MCP サーバーとその説明」「ファイルシステム・サンドボックス・ブラウザー」「サブエージェントの起動や引き継ぎ、モデルの切り替え」「圧縮・継続・リンターなどを決まった形で走らせるフックとミドルウェア」を挙げています。上の表は、それを Claude Code などのドキュメントの用語に合わせて並べ直したものです。

ハーネスエンジニアリングとは

ハーネスエンジニアリング(harness engineering)は、モデルを変えずに、ハーネス(道具・指示・制約・検査の仕組み)を設計して、エージェントの結果を安定させる取り組みです。 OpenAI の記事は、エンジニアの仕事が「コードを書くこと」から "designing environments, feedback loops, and control systems"(環境・フィードバックの輪・制御の仕組みを設計すること)に移ったと書き、"Humans steer. Agents execute."(人が舵を取り、エージェントが実行する)とまとめています。

言葉の出どころ(時系列)

日付 出典 内容
2025年11月26日 Anthropic「Effective harnesses for long-running agents」 長時間の作業向けに、初期化用と作業用の2つのエージェント、進捗ファイル、機能一覧の JSON、init.sh、git を組み合わせる
2026年2月5日 Mitchell Hashimoto 氏「My AI Adoption Journey」 Step 5「Engineer the Harness」で "I've grown to calling this harness engineering" と書く
2026年2月11日 OpenAI「Harness engineering: leveraging Codex in an agent-first world」 約5か月、手書きのコード0行で約100万行・約1,500件のプルリクエストを作った報告
2026年3月10日 LangChain「The Anatomy of an Agent Harness」 「Agent = Model + Harness」とハーネスの部品の整理
2026年4月2日 martinfowler.com「Harness engineering for coding agent users」 利用者が作るハーネスを、ガイドとセンサーに分けて整理

Hashimoto 氏の記事には「業界で広く使われる言葉があるかは分からない」という趣旨の一文もあり、言葉そのものは2026年に広まったものです。OpenAI の記事では、リポジトリの入口の AGENTS.md を約100行に抑え、詳しい内容は docs/ 以下の文書へ案内する形にしています。

ガイドとセンサー(martinfowler.com の整理)

Böckeler 氏は、利用者が作るハーネスを2種類に分けています。

種類 いつ効くか 例
ガイド(feedforward) 行動の前。最初の一手をよくする AGENTS.md・CLAUDE.md、スキル、設計の文書
センサー(feedback) 行動の後。結果を見て直させる テスト・リンター・型チェック・構造の解析
センサーのうち推論で判定するもの 行動の後。意味を見て判定する AI によるコードレビュー、LLM as judge

テストやリンターのような「計算で判定するもの(computational)」は速く、結果がぶれません。AI レビューのような「推論で判定するもの(inferential)」は遅く費用がかかり、結果がぶれやすいと書かれています。同じ失敗が続くときは、指示の文言を足すより、センサー(テストやリンター)をフックで必ず走らせるほうが確実というのが、この整理から出てくる考え方です。

関連記事「Claude Code Hooksの使い方|設定例と実行結果・止め方まで」で扱っています。

プロンプトエンジニアリング・コンテキストエンジニアリングとの違い

3つは別々のものではなく、プロンプトはコンテキストの一部、コンテキストはハーネスが組み立てるものの一部という入れ子の関係です。

観点 プロンプトエンジニアリング コンテキストエンジニアリング ハーネスエンジニアリング
設計するもの 指示の文言と構成 モデルに見せる情報の全体 道具・許可・ループ・検査を含む実行の仕組み
主な問い 何を、どう書くか 何を見せ、何を外すか 何を持たせ、どう確かめ、どこで止めるか
例 システムプロンプト、Few-shot 指示ファイル、道具の説明、会話の要約 権限ルール、フック、最大回数、サブエージェント
効くタイミング 1回の呼び出し 呼び出しのたびに組み直す エージェントが動いている間ずっと
失敗したときの直し方 文言を直す 入れる情報を減らす・並べ替える 道具・制約・テストを足す

Anthropic の「Effective context engineering for AI agents」(2025年9月29日)は、プロンプトエンジニアリングを "methods for writing and organizing LLM instructions for optimal outcomes"、コンテキストエンジニアリングを "the set of strategies for curating and maintaining the optimal set of tokens (information) during LLM inference" と定義しています。ハーネスは、そのコンテキストを毎回組み立てて送り、返ってきた道具の呼び出しを実行する側です。

関連記事「コンテキストエンジニアリングとは プロンプトとの違いと設計方法」で扱っています。

Pythonで最小のハーネスを作る(102行)

部品を自分で書くと、ハーネスが何をしているかがはっきりします。標準ライブラリだけで、次の5つを持つハーネスを作りました。

  • 道具2つ(read_file・run_command)の定義と実行
  • 許可(読むだけは自動で通す、コマンドは人に聞く、危ない形は先に断る)
  • フック(道具の前後に必ず走る処理)
  • 記憶(AGENTS.md をシステムプロンプトに足す)
  • ループと最大回数
1

擬似の LLM サーバーを立てる

本物の API の代わりに、決まった筋書きで道具の呼び出しを返す OpenAI 互換のサーバーを mini_mock.py として起動します。

本物の API につなぐときは環境変数 BASE_URL・MODEL・API_KEY を変える

2

harness.py を置く

道具・許可・フック・記憶・ループの5つを、それぞれ関数に分けて書きます。

102行。外部のライブラリは使わない

3

作業フォルダに AGENTS.md と hello.txt を置く

AGENTS.md に書いた決まりが、毎回システムプロンプトに入ることを確かめます。

消されては困るものとして build フォルダも作る

4

指示を変えて実行する

読む・コマンドを許可する・断る・拒否リストに当たる・止まらない、の5通りを試します。

出力の [hook:pre] と [tool] の行で、ハーネスの判断が見える

擬似のLLMサーバー(mini_mock.py)

指示の文に「消して」「コマンド」が入っていれば run_command を、それ以外は read_file を呼ぶように返します。道具の結果が届いたら文章で答えます。「ループ」と書くと、いつまでも道具を呼び続けます。

"""動作確認用の擬似 LLM サーバー(OpenAI 互換・ストリーミングなし)。"""
import json, sys
from http.server import BaseHTTPRequestHandler, HTTPServer

class H(BaseHTTPRequestHandler):
    def log_message(self, *a):
        pass

    def do_POST(self):
        body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        msgs = body["messages"]
        task = next(m["content"] for m in msgs if m["role"] == "user")
        if msgs[-1]["role"] == "tool" and "ループ" not in task:
            msg = {"role": "assistant", "content": "結果: " + msgs[-1]["content"].strip()}
        else:
            if "消して" in task:
                name, args = "run_command", {"command": "rm -rf build"}
            elif "コマンド" in task:
                name, args = "run_command", {"command": "ls"}
            else:
                name, args = "read_file", {"path": "hello.txt"}
            msg = {"role": "assistant", "content": None, "tool_calls": [{
                "id": f"call_{len(msgs)}", "type": "function",
                "function": {"name": name, "arguments": json.dumps(args)}}]}
        out = json.dumps({"choices": [{"index": 0, "message": msg}]}).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(out)))
        self.end_headers()
        self.wfile.write(out)

HTTPServer(("127.0.0.1", int(sys.argv[1]) if len(sys.argv) > 1 else 8000), H).serve_forever()

ハーネス本体(harness.py)

"""最小のエージェントハーネス(標準ライブラリだけ)。
モデルは OpenAI 互換の /v1/chat/completions。ハーネスが受け持つのは
道具の定義と実行・許可の確認・フック・記憶(AGENTS.md)・ループと最大回数。
"""
import json, os, subprocess, sys, urllib.request

BASE_URL = os.environ.get("BASE_URL", "http://127.0.0.1:8000/v1")
MODEL = os.environ.get("MODEL", "mock-coder")
MAX_STEPS = int(os.environ.get("MAX_STEPS", "5"))     # ループの上限
MAX_OUTPUT = 2000                                     # 道具の結果をモデルに返す最大文字数

# 1. 道具の定義(モデルに見せる JSON Schema)
TOOLS = [
    {"type": "function", "function": {
        "name": "read_file", "description": "テキストファイルを読む",
        "parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}},
    {"type": "function", "function": {
        "name": "run_command", "description": "シェルのコマンドを実行する",
        "parameters": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}},
]

# 2. 道具の実行(実際に手を動かすのはハーネス)
def read_file(path):
    with open(path, encoding="utf-8") as f:
        return f.read()

def run_command(command):
    p = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30)
    return p.stdout + p.stderr

IMPL = {"read_file": read_file, "run_command": run_command}

# 3. 許可:読むだけの道具は自動で通し、コマンドは人に聞く。危ない形は先に断る
AUTO_ALLOW = {"read_file"}
DENY_WORDS = ["rm ", "sudo ", "curl "]

def allowed(name, args):
    if name in AUTO_ALLOW:
        return True, "自動で許可"
    cmd = args.get("command", "")
    if any(w in cmd for w in DENY_WORDS):
        return False, "拒否リストに一致"
    ans = input(f"  [許可] {name} {json.dumps(args, ensure_ascii=False)} を実行しますか? (y/n) ")
    if not sys.stdin.isatty():
        print(ans)
    ok = ans.strip().lower() == "y"
    return ok, "人が許可" if ok else "人が拒否"

# 4. フック:道具の前後に必ず走る処理(ここでは記録だけ)
def pre_tool_hook(name, args):
    print(f"  [hook:pre]  {name} {json.dumps(args, ensure_ascii=False)}")

def post_tool_hook(name, result):
    print(f"  [hook:post] {name} -> {len(result)} 文字")

# 5. 記憶:AGENTS.md があればシステムプロンプトに足す
def system_prompt():
    s = "あなたはコーディングを手伝うエージェントです。道具を使って作業します。"
    if os.path.exists("AGENTS.md"):
        s += "\n\n# AGENTS.md\n" + read_file("AGENTS.md")
    return s

def call_model(messages):
    body = json.dumps({"model": MODEL, "messages": messages, "tools": TOOLS}).encode()
    req = urllib.request.Request(BASE_URL + "/chat/completions", data=body,
                                 headers={"Content-Type": "application/json",
                                          "Authorization": "Bearer " + os.environ.get("API_KEY", "sk-local")})
    with urllib.request.urlopen(req, timeout=60) as r:
        return json.load(r)["choices"][0]["message"]

# 6. ループ:モデルが道具を求める限り、実行して結果を返し、また聞く
def run(task):
    messages = [{"role": "system", "content": system_prompt()}, {"role": "user", "content": task}]
    for step in range(1, MAX_STEPS + 1):
        msg = call_model(messages)
        messages.append(msg)
        calls = msg.get("tool_calls") or []
        print(f"[step {step}] messages {len(messages) - 1} 件を送信 / tool_calls {len(calls)} 件")
        if not calls:
            print("[answer]", msg.get("content"))
            return
        for c in calls:
            name = c["function"]["name"]
            args = json.loads(c["function"]["arguments"] or "{}")
            pre_tool_hook(name, args)
            ok, why = allowed(name, args)
            if not ok:
                result = f"拒否されました({why})"
            elif name not in IMPL:
                result = f"未知の道具: {name}"
            else:
                try:
                    result = IMPL[name](**args)
                except Exception as e:
                    result = f"{type(e).__name__}: {e}"
            print(f"  [tool] {name}: {why}")
            post_tool_hook(name, result)
            messages.append({"role": "tool", "tool_call_id": c["id"], "content": result[:MAX_OUTPUT]})
    print(f"[stop] 最大回数 {MAX_STEPS} に達したので止めました")

if __name__ == "__main__":
    run(" ".join(sys.argv[1:]) or "hello.txt を読んで")

作業フォルダの準備と起動です。

mkdir work && cd work
printf 'こんにちは、ハーネス。\n' > hello.txt
printf -- '- 返事は日本語で短く。\n- テストは pytest で実行する。\n' > AGENTS.md
mkdir build && touch build/app.bin
python3 ../mini_mock.py 8000 &

道具を呼んで結果を返す(ループ2周)

$ python3 ../harness.py "hello.txt を読んで"
[step 1] messages 2 件を送信 / tool_calls 1 件
  [hook:pre]  read_file {"path": "hello.txt"}
  [tool] read_file: 自動で許可
  [hook:post] read_file -> 12 文字
[step 2] messages 4 件を送信 / tool_calls 0 件
[answer] 結果: こんにちは、ハーネス。

1周目でモデルが read_file を求め、ハーネスがファイルを読みました。2周目は、道具の呼び出しと結果の2件を足した4件を送り、モデルが文章で答えて終わりました。「道具を実行して結果を足し、もう一度送る」の繰り返しが、エージェントループの正体です。

許可を求める(y と n)

run_command は自動では通さず、人に聞きます。

$ echo y | python3 ../harness.py "コマンドで一覧を出して"
[step 1] messages 2 件を送信 / tool_calls 1 件
  [hook:pre]  run_command {"command": "ls"}
  [許可] run_command {"command": "ls"} を実行しますか? (y/n) y
  [tool] run_command: 人が許可
  [hook:post] run_command -> 26 文字
[step 2] messages 4 件を送信 / tool_calls 0 件
[answer] 結果: AGENTS.md
build
hello.txt
$ echo n | python3 ../harness.py "コマンドで一覧を出して"
[step 1] messages 2 件を送信 / tool_calls 1 件
  [hook:pre]  run_command {"command": "ls"}
  [許可] run_command {"command": "ls"} を実行しますか? (y/n) n
  [tool] run_command: 人が拒否
  [hook:post] run_command -> 13 文字
[step 2] messages 4 件を送信 / tool_calls 0 件
[answer] 結果: 拒否されました(人が拒否)

断ったときも、エラーで止めずに「拒否されました」という結果をモデルへ返しています。モデルは断られたことを知ったうえで、次の手を考えられます。後で見る OpenCode・Cline も、断ったときは同じように理由を道具の結果として返していました。

拒否リストで先に止める

$ python3 ../harness.py "build を消して"
[step 1] messages 2 件を送信 / tool_calls 1 件
  [hook:pre]  run_command {"command": "rm -rf build"}
  [tool] run_command: 拒否リストに一致
  [hook:post] run_command -> 17 文字
[step 2] messages 4 件を送信 / tool_calls 0 件
[answer] 結果: 拒否されました(拒否リストに一致)
$ ls build
app.bin

人に聞く前に断ったので、build は残っています。ただし、この例のような文字列の一致だけでは、書き方を変えたコマンドに当たりません。Claude Code のドキュメントは、Bash のルールは && などでつないだコマンドを分割してから照合すると説明しています。本格的に止めたいときは、コマンドの解釈・作業フォルダの外への書き込みの禁止・サンドボックスを組み合わせます。

最大回数で止める

$ MAX_STEPS=3 python3 ../harness.py "ループして"
[step 1] messages 2 件を送信 / tool_calls 1 件
  [hook:pre]  read_file {"path": "hello.txt"}
  [tool] read_file: 自動で許可
  [hook:post] read_file -> 12 文字
[step 2] messages 4 件を送信 / tool_calls 1 件
  [hook:pre]  read_file {"path": "hello.txt"}
  [tool] read_file: 自動で許可
  [hook:post] read_file -> 12 文字
[step 3] messages 6 件を送信 / tool_calls 1 件
  [hook:pre]  read_file {"path": "hello.txt"}
  [tool] read_file: 自動で許可
  [hook:post] read_file -> 12 文字
[stop] 最大回数 3 に達したので止めました

モデルが同じ道具を呼び続けても、ハーネスが3周で止めました。送る件数は 2→4→6 と増えています。ループが続くほど1回の送信が大きくなり、費用も増えます。 上限は、止まらないときの安全装置です。

実際に送られたJSON

2周目に送った本文を、届いた中身を記録する擬似サーバー(次の節で使うもの)で取りました(1,323 バイト)。道具の呼び出し(assistant)と結果(tool)が会話に足され、tools は毎回同じものが付きます。

{
  "model": "mock-coder",
  "messages": [
    {"role": "system", "content": "あなたはコーディングを手伝うエージェントです。道具を使って作業します。\n\n# AGENTS.md\n- 返事は日本語で短く。\n- テストは pytest で実行する。\n"},
    {"role": "user", "content": "hello.txt を読んで"},
    {"role": "assistant", "content": null, "tool_calls": [
      {"id": "call_81375", "type": "function", "function": {"name": "read_file", "arguments": "{\"path\": \"hello.txt\"}"}}
    ]},
    {"role": "tool", "tool_call_id": "call_81375", "content": "こんにちは、ハーネス。\n"}
  ],
  "tools": [
    {"type": "function", "function": {"name": "read_file", "description": "テキストファイルを読む", "parameters": {"…": "…"}}},
    {"type": "function", "function": {"name": "run_command", "description": "シェルのコマンドを実行する", "parameters": {"…": "…"}}}
  ]
}

(parameters の中身は harness.py の TOOLS と同じなので省略しています。)

AGENTS.md の中身がシステムプロンプトに入っていることが分かります。「記憶」と呼ばれる部品の多くは、このようにファイルを読んで会話に差し込んでいるだけです。

実際のハーネスは何を送っているか(OpenCode・Clineで計測)

同じ擬似サーバー(こちらはストリーミングの応答にも対応したもの)に OpenCode と Cline をつなぎ、同じ作業フォルダで「hello.txt を読んで」と指示して、届いたリクエストを比べました。

OpenCode は ~/.config/opencode/opencode.json に OpenAI 互換の提供元を書き、opencode run で1回だけ実行しました。

{
  "$schema": "https://opencode.ai/config.json",
  "model": "local/mock-coder",
  "provider": {
    "local": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "mock",
      "options": { "baseURL": "http://127.0.0.1:26801/v1", "apiKey": "sk-local-test" },
      "models": { "mock-coder": { "name": "Mock Coder", "limit": { "context": 128000, "output": 8192 } } }
    }
  }
}

Cline は cline auth で同じ擬似サーバーを登録しました。CLI は裏で「ハブ」というプロセスを起動するので、ポートを CLINE_HUB_PORT で指定し、終わったら cline hub stop で止めています。

export CLINE_HUB_PORT=26890
cline auth -p openai -k sk-local-test -m mock-coder -b http://127.0.0.1:26801/v1
cline "hello.txt を読んで"
cline hub stop
Provider configured: openai-compatible (mock-coder)
[read_files] {"files":[{"path":"hello.txt"}]}
   ⎿ 1 | こんにちは、ハーネス。
{"stopped":true}
30,835バイトOpenCode の1回目の本体の送信うち 69% が道具10個の定義
25個Cline が渡した道具うち18個がチーム機能の道具
1,017バイト自作ハーネスの1回目の送信道具2個・システムプロンプト83文字
項目 自作ハーネス OpenCode 1.19.3 Cline 3.0.73
渡した道具の数 2 10 25(Plan では24)
システムプロンプト 83 文字 9,643 文字 4,333 文字(Plan では6,094)
1回目の送信の大きさ 1,017 バイト 30,835 バイト 21,850 バイト
うち道具の定義 432 バイト 21,143 バイト 18,076 バイト
道具1回の作業の送信回数 2回 3回(タイトル作成1回を含む) 2回

大きさは、届いた本文(JSON)のバイト数です。ひと言の指示でも、実際のハーネスは2万〜3万バイトを送っていて、その7〜8割は道具の定義でした。 道具の結果を受け取るたびに、これを毎回送り直します。

渡された道具の名前

ハーネス 道具の名前(届いた順)
OpenCode bash・edit・glob・grep・read・skill・task・todowrite・webfetch・write
Cline(Act) read_files・search_codebase・run_commands・fetch_web_content・editor・ask_question・spawn_agent・team_ で始まる18個
Cline(Plan) 上から editor を除いた24個

道具の引数も、ハーネスごとに違いました。ファイルを読む道具は、OpenCode の read が filePath・offset・limit、Cline の read_files が files(配列)を受け取ります。OpenCode で説明文がいちばん長い道具は bash で 4,627 文字、次が task(サブエージェントを呼ぶ道具)の 3,019 文字でした。

AGENTS.mdはどこに入るか

3つとも、作業フォルダの AGENTS.md をシステムプロンプトの中に入れていました。入れ方の見出しが違います。

ハーネス システムプロンプト内の入り方
自作 # AGENTS.md の見出しの下
OpenCode Instructions from: ~/work/AGENTS.md の行の下
Cline # Rules → ## Workspace AGENTS.md の下

(ホームのパスは ~ に置き換えています。)

道具の結果の返し方

同じ1行のファイルを読んだ結果が、モデルにはこう渡されていました。行番号や成功の印を付けるのもハーネスの仕事です。

OpenCode:
<path>~/work/hello.txt</path>
<type>file</type>
<content>
1: こんにちは、ハーネス。

(End of file - total 1 lines)
</content>

Cline:
[{"query":"hello.txt","result":"1 | こんにちは、ハーネス。","success":true}]

許可を変えたときの動き

試したこと 起きたこと
OpenCode で "permission": {"read": "deny"} read が道具の一覧から消え、10個から9個になった
OpenCode で "permission": {"bash": "ask"} にして opencode run permission requested: bash (ls); auto-rejecting と出て、自動で断った
Cline で --auto-approve false を付けて実行 道具の結果として「対話できないので承認できない」というエラーを返した
Cline で -p(Plan モード) 編集の道具 editor が一覧から消えた

OpenCode で断ったときにモデルへ返った結果は The user rejected permission to use this specific tool call.、Cline は次の文でした。

{"error":"Tool approval requires an interactive session, but this session is non-interactive. -- NOT a tool or system failure. Clarify with user before proceeding."}

「断る」には、道具を一覧から外してモデルに見せない方法と、呼ばれた後に断って理由を返す方法の2つがあります。 見せなければ送信も小さくなります。Claude Code のドキュメントでも、--disallowedTools に道具の名前だけを書くとその道具が文脈から外れ、Bash(rm *) のような形で書くと道具は残したまま一致した呼び出しだけを断ると説明されています。

Claude Code・OpenCode・Clineのハーネスの部品比較

各ドキュメントの記載を、冒頭の部品の順に並べます(2026年10月18日時点)。

部品 Claude Code OpenCode Cline
組み込みの道具 Read・Edit・Write・Bash・Grep・Glob・WebFetch など40以上(条件付きを含む) bash・edit・write・read・grep・glob・apply_patch・skill・webfetch・websearch・question・lsp(試験的) read_files・search_codebase・run_commands・fetch_web_content・apply_patch・editor・skills・ask_question・submit_and_exit
道具を足す MCP・Skills・プラグイン MCP・カスタムツール MCP・Skills・プラグイン
許可 allow/ask/deny のルール。deny→ask→allow の順に評価。6つの権限モード allow/ask/deny。最後に一致したルールが勝つ 項目ごとの自動承認と YOLO モード。CLI は既定で自動承認がオン
指示ファイル CLAUDE.md(AGENTS.md も読める)・自動メモリー AGENTS.md(無ければ CLAUDE.md) .clinerules/・.cline/rules/・AGENTS.md
フック 33のイベント(PreToolUse など)・5つの種類 プラグインの tool.execute.before/after など SDK プラグインの beforeTool/afterTool など
サブエージェント Agent ツール task ツール(General・Explore・Scout) spawn_agent(CLI と Desktop)
ループの上限 -p 実行時の --max-turns(既定は上限なし)・--max-budget-usd エージェントごとの steps --retries(連続の失敗の回数。既定3)
戻す仕組み チェックポイント(Esc を2回) /undo・/redo(Git を使う) チェックポイント(プロジェクトとは別の Git リポジトリ)

表から読み取れる、3つに共通する点と違う点です。

  • 共通:どれも「道具の定義を渡し、呼ばれたら許可を確かめて実行し、結果を返す」ループを持ち、MCP で道具を足せます。指示ファイルとして AGENTS.md を読める点も共通です。
  • 許可の決め方:Claude Code は deny が最優先で、範囲の広い deny は狭い allow でも上書きできません。OpenCode は最後に一致したルールが勝つので、"*" を先頭に書きます。Cline は道具の種類ごとの自動承認の切り替えです。
  • 同じ呼び出しの繰り返し:OpenCode には doom_loop という許可の項目があり、同じ入力で同じ道具が3回続くと発動します(既定は ask)。
  • フックの止め方:Claude Code の PreToolUse フックは、終了コード2か、permissionDecision に "deny" を返すと道具の呼び出しを止めます。Cline の beforeTool は { skip: true, reason } を返すと止まり、理由がモデルに返ります。

Claude Code の権限の書き方は、関連記事「Claude Code settings.json設定一覧|権限の雛形と優先順位」で扱っています。サブエージェントで文脈を分ける効果は、関連記事「Claude Codeサブエージェントの使い方|実測で見る委任とコンテキスト節約」で扱っています。

ハーネスを設計するときのチェックリスト

自分のプロジェクトで Claude Code などを使うときも、ハーネスの部品のうち「利用者が作る部分」(指示ファイル・権限・フック・テスト)は自分で設計します。次の7つを確かめます。

  • 1指示ファイルは短く、間違えやすいことだけを書いているOpenAI は入口の AGENTS.md を約100行にし、詳細は docs/ へ案内した
  • 2消す・送る・公開する操作は、ルールで ask か deny にしている読むだけの道具は自動、書き換えと外への通信は確認、が基本の形
  • 3テスト・リンター・型チェックを、エージェントが自分で走らせられるセンサーがあれば、モデルが結果を見て自分で直せる
  • 4毎回守らせたい処理は、指示ではなくフックにしている整形・リンター・記録は、モデルの判断に任せず必ず走らせる
  • 5無人で動かすときの回数・費用の上限を決めているClaude Code は --max-turns・--max-budget-usd、OpenCode は steps
  • 6使わない道具や MCP サーバーを外している道具の定義は毎回送られ、送信の7〜8割を占めていた
  • 7長い作業は、進捗をファイルに残して次の回へ引き継げるAnthropic は進捗ファイルと機能一覧の JSON、git のコミットを使った

よくある失敗とハーネスでの直し方

症状 原因(ハーネスの部品) 直し方
同じ道具を呼び続けて終わらない ループに上限が無い 最大回数を決める。OpenCode は doom_loop、Claude Code は --max-turns
危ないコマンドを確認なしで実行しそうになる 許可のルールが広すぎる 消す・送る操作を deny か ask にする。PreToolUse フックで止める
同じ規約違反を何度も繰り返す 指示(ガイド)だけで、検査(センサー)が無い リンター・テストを PostToolUse などのフックで必ず走らせる
長い会話の途中で前の指示を忘れる 文脈があふれて要約で落ちた 守らせたい決まりは指示ファイルに書く。調べ物はサブエージェントへ
長い作業が次の回に引き継がれない 会話が回ごとに新しくなる 進捗ファイル・機能一覧・git のコミットに状態を残す
無人の実行で、道具が毎回断られる 承認できる人がいない 事前に allow のルールを決めておく。断られた理由は道具の結果に出る
ひと言の指示でも送信が大きい 道具と MCP の定義が毎回付く 使わない道具を外す。deny で外すと一覧から消える

試したときに出たメッセージ

ハーネス メッセージ(原文) 意味と直し方
OpenCode permission requested: bash (ls); auto-rejecting opencode run で ask の道具が呼ばれ、自動で断った。使わせるなら allow にする
OpenCode The user rejected permission to use this specific tool call. 断られたときにモデルへ返る結果
Cline Tool approval requires an interactive session, but this session is non-interactive. --auto-approve false で、承認する人がいない。対話の端末で動かすか、自動承認の範囲を決める
Cline Unknown command or unquoted prompt: コマンドで一覧を出して 空白の無い1語の指示が、サブコマンドの名前として解釈された。空白を含む文にする

よくある質問

Q. ハーネスとは何ですか? A. AI の分野では、エージェントのうち LLM 本体以外の仕組み全部(道具の実行・許可・記憶・ループ・フックなど)を指します。電気のワイヤーハーネスや高所作業のフルハーネスとは別物です。「「ハーネス」の意味の見分け表」で見分け方をまとめています。

Q. ハーネスエンジニアリングとは何ですか? A. モデルを変えずに、道具・指示ファイル・権限・テストやリンターなどの検査を設計して、エージェントの結果を安定させる取り組みです。2026年2月の Mitchell Hashimoto 氏と OpenAI の記事で広まりました。

Q. プロンプトエンジニアリング・コンテキストエンジニアリングとの違いは? A. プロンプトは指示の文言、コンテキストはモデルに見せる情報の全体、ハーネスはそれを組み立てて送り、道具を実行して止めるところまでの仕組みです。「プロンプトエンジニアリング・コンテキストエンジニアリングとの違い」の表で比べています。

Q. Claude Code はハーネスですか? A. Claude Code のドキュメントは、モデルの周りで道具を用意し、モデルが見る文脈を管理する層を "agentic harness" と呼んでいます。Claude Code 自体が、Claude のモデルにかぶせたハーネスです。

Q. ハーネスは自分で作れますか? A. 作れます。この記事の harness.py は、標準ライブラリだけの102行で、道具・許可・フック・記憶・ループの上限を持っています。製品のハーネスとの違いは、道具の数・権限の細かさ・文脈の圧縮・戻す仕組みなどです。

Q. ハーネスの部品で、利用者が手を入れられるのはどこですか? A. 指示ファイル(AGENTS.md・CLAUDE.md)、権限のルール、フック、MCP で足す道具、サブエージェントの定義が主なところです。martinfowler.com の記事は、これを利用者が作るハーネスと呼んでいます。

まとめ

  • ハーネスは「Agent = Model + Harness」のうちモデル以外の全部。道具・許可・ループ・記憶・コンテキスト管理・フック・サブエージェント・戻す仕組みを受け持つ。
  • モデルは道具を使いたいと JSON で書くだけで、実行・許可・停止はハーネスが行う。102行の Python で、その流れを全部再現できた。
  • ハーネスエンジニアリングは、指示(ガイド)と検査(センサー)を設計してエージェントを安定させる取り組み。2026年に広まった言葉。
  • OpenCode は道具10個・30,835 バイト、Cline は道具25個・21,850 バイトを、ひと言の指示で送っていた。7〜8割が道具の定義だった。
  • 断り方には「道具を一覧から外す」と「呼ばれた後に断って理由を返す」の2つがある。無人の実行では、ask の道具は自動で断られた。

参考・出典

確認日はいずれも 2026年10月18日です。

  • LangChain「The Anatomy of an Agent Harness」:https://www.langchain.com/blog/the-anatomy-of-an-agent-harness
  • martinfowler.com「Harness engineering for coding agent users」:https://martinfowler.com/articles/exploring-gen-ai/harness-engineering.html
  • OpenAI「Harness engineering: leveraging Codex in an agent-first world」:https://openai.com/index/harness-engineering/
  • Anthropic「Effective harnesses for long-running agents」:https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents
  • Anthropic「Effective context engineering for AI agents」:https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
  • Mitchell Hashimoto「My AI Adoption Journey」:https://mitchellh.com/writing/my-ai-adoption-journey
  • Claude Code「How Claude Code works」:https://code.claude.com/docs/en/how-claude-code-works
  • Claude Code「Tools reference」:https://code.claude.com/docs/en/tools-reference
  • Claude Code「Configure permissions」:https://code.claude.com/docs/en/permissions
  • Claude Code「Hooks reference」:https://code.claude.com/docs/en/hooks
  • Claude Code「CLI reference」:https://code.claude.com/docs/en/cli-reference
  • OpenCode「Tools」:https://opencode.ai/docs/tools/
  • OpenCode「Permissions」:https://opencode.ai/docs/permissions/
  • OpenCode「Agents」:https://opencode.ai/docs/agents/
  • OpenCode「Plugins」:https://opencode.ai/docs/plugins/
  • OpenCode「Rules」:https://opencode.ai/docs/rules/
  • OpenCode「TUI」:https://opencode.ai/docs/tui/
  • Cline「Tools」:https://docs.cline.bot/tools-reference/all-cline-tools
  • Cline「CLI Reference」:https://docs.cline.bot/cli/cli-reference
  • Cline「Rules」:https://docs.cline.bot/customization/cline-rules
  • Cline「Auto Approve & YOLO Mode」:https://docs.cline.bot/features/auto-approve
  • Cline「Subagents」:https://docs.cline.bot/features/subagents
  • Cline「Checkpoints」:https://docs.cline.bot/core-workflows/checkpoints
  • Cline SDK「Plugins」:https://docs.cline.bot/sdk/plugins
  • Wikipedia「Test harness」:https://en.wikipedia.org/wiki/Test_harness
  • Wikipedia「ワイヤーハーネス」:https://ja.wikipedia.org/wiki/ワイヤーハーネス

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「ハーネスとは|AIエージェントの仕組みとハーネスエンジニアリング入門」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?