n4nAI

How to build a coding agent with Claude Opus 4.8

Step-by-step tutorial for building a Claude Opus 4.8 coding agent with sandboxed tool use, conversation loop, and OpenAI-compatible API access.

n4n Team4 min read929 words

Audio narration

Coming soon — every post will get a voice note here.

Building a claude opus 4.8 coding agent is less about prompting tricks and more about giving the model a tight, observable loop over a real filesystem and shell. In this tutorial we stand up a minimal agent that can read files, run tests, and patch code using the model’s native tool-calling via an OpenAI-compatible endpoint. You’ll end with a runnable Python script that drives a real coding task end to end, and a clear picture of where to harden it before production.

Prerequisites

  • Python 3.11 or newer. The subprocess timeout behavior and model_dump helper we use are cleanest on 3.11+.
  • openai Python package (>=1.40): pip install openai.
  • An API key for a gateway that exposes Claude Opus 4.8. We’ll point the OpenAI client at a single OpenAI-compatible endpoint; n4n.ai fronts 240+ models including this one and handles provider fallback automatically when a backend is rate-limited.
  • A throwaway working directory. The agent will execute shell commands, so never point it at a directory you care about. A temp dir is fine.
export OPENAI_API_KEY="sk-..."
export BASE_URL="https://api.n4n.ai/v1"
export WORKDIR="/tmp/agent_sandbox"
mkdir -p "$WORKDIR"

If you use a different gateway, just change BASE_URL and the model slug. The code below assumes the OpenAI-compatible contract: same request/response shapes, tools as function schemas.

The agent loop in practice

The pattern is boring and that’s correct:

  1. Send system prompt + conversation to model with a tool schema.
  2. Model returns either a final message or one or more tool_calls.
  3. You execute the calls locally, capture stdout/stderr/result, and append them as tool messages.
  4. Repeat until the model stops requesting tools or you hit a step cap.

No autonomous recursion beyond a max step count. For a coding agent, 20–30 steps is plenty for a single focused task like fixing a test or scaffolding a module. If you need more, the task is underspecified.

Define the tool schema

Claude Opus 4.8 accepts JSON-schema tool definitions through the OpenAI-compatible /chat/completions interface. We expose three primitives: read a file, write a file, run a shell command. Keep the schema strict—the model follows descriptions literally.

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "Read a UTF-8 text file relative to the workdir. Returns contents.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "Relative path"}
                },
                "required": ["path"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "write_file",
            "description": "Write content to a file, overwriting if present.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "content": {"type": "string"},
                },
                "required": ["path", "content"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "run_shell",
            "description": "Run a shell command in the workdir. Returns combined output.",
            "parameters": {
                "type": "object",
                "properties": {
                    "cmd": {"type": "string"},
                    "timeout": {"type": "integer", "default": 30},
                },
                "required": ["cmd"],
            },
        },
    },
]

Implement sandboxed execution

Keep the agent inside WORKDIR. Use subprocess.run with cwd, capture output, enforce a timeout. This is not a security boundary against malicious model output—just a footgun guard so it can’t rm -rf your home directory.

import os, subprocess, json

WORKDIR = os.environ.get("WORKDIR", "/tmp/agent_sandbox")

def read_file(path: str) -> str:
    full = os.path.join(WORKDIR, path)
    with open(full, "r", encoding="utf-8") as f:
        return f.read()

def write_file(path: str, content: str) -> str:
    full = os.path.join(WORKDIR, path)
    os.makedirs(os.path.dirname(full), exist_ok=True)
    with open(full, "w", encoding="utf-8") as f:
        f.write(content)
    return f"wrote {len(content)} bytes to {path}"

def run_shell(cmd: str, timeout: int = 30) -> str:
    try:
        res = subprocess.run(
            cmd, shell=True, cwd=WORKDIR, capture_output=True,
            text=True, timeout=timeout
        )
        out = res.stdout + res.stderr
        return f"exit={res.returncode}\n{out[:4000]}"
    except subprocess.TimeoutExpired:
        return "exit=timeout"

Wire the client

We use the OpenAI Python client but swap base_url. The model name follows the gateway’s routing convention; for Claude Opus 4.8 it’s typically anthropic/claude-opus-4.8. If your gateway honors client routing directives, you can pin a provider or region in the same field.

from openai import OpenAI

client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    base_url=os.environ["BASE_URL"],
)
MODEL = "anthropic/claude-opus-4.8"

The loop

We keep a messages list. System prompt is terse and mandates tool use for any file access. Note the exclude_none=True on model_dump—the OpenAI client objects include None fields that some gateways reject.

SYSTEM = """You are a coding agent. Use the provided tools to inspect and modify code in the workdir.
Do not guess file contents. When the task is complete, reply with a final summary and no tool calls.
"""

def run_agent(task: str, max_steps: int = 25):
    messages = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": task},
    ]
    for step in range(max_steps):
        resp = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            tools=TOOLS,
            tool_choice="auto",
        )
        msg = resp.choices[0].message
        if not msg.tool_calls:
            print("FINAL:", msg.content)
            return msg.content
        messages.append(msg.model_dump(exclude_none=True))
        for call in msg.tool_calls:
            fn = call.function.name
            args = json.loads(call.function.arguments)
            if fn == "read_file":
                result = read_file(args["path"])
            elif fn == "write_file":
                result = write_file(args["path"], args["content"])
            elif fn == "run_shell":
                result = run_shell(args.get("cmd"), args.get("timeout", 30))
            else:
                result = "unknown tool"
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": result,
            })
    return "step limit reached"

Expected first checkpoint output when run with a trivial task like “list files in workdir”:

FINAL: I inspected the directory and found no source files. The workdir is empty.

A real task: fix a failing test

Seed the sandbox with a buggy module and a pytest file.

cat > "$WORKDIR/calc.py" <<'PY'
def add(a, b):
    return a - b  # bug: should be +
PY

cat > "$WORKDIR/test_calc.py" <<'PY'
from calc import add
def test_add():
    assert add(2, 3) == 5
PY

Now drive the agent:

task = (
    "The test test_calc.py fails. Read both files, fix calc.py so the test "
    "passes, then run pytest to confirm. Report the final diff."
)
run_agent(task)

A competent claude opus 4.8 coding agent will typically:

  1. read_file on calc.py and test_calc.py.
  2. write_file to correct the operator.
  3. run_shell pytest -q.
  4. Return a final summary with the diff.

Sample transcript excerpt:

tool_call: read_file path=calc.py
tool_result: def add(a, b):\n    return a - b
tool_call: write_file path=calc.py content="def add(a, b):\n    return a + b\n"
tool_result: wrote 28 bytes to calc.py
tool_call: run_shell cmd="pytest -q"
tool_result: exit=0\n1 passed in 0.01s
FINAL: Fixed calc.py by changing '-' to '+'. pytest now passes (1 passed).

Tool design principles

The three tools above are deliberately narrow. Wide tools (“edit code”) invite the model to emit malformed patches. Narrow tools with explicit feedback loops are easier to debug: every action is a line in the transcript. If you need edits, prefer write_file with full content over sed invocations—the model is better at regenerating a file than at counting lines.

Also, never return huge file contents in run_shell without truncation. We cap at 4000 chars; the model can ask for specific files via read_file if it needs more.

Debugging the agent

When the loop misbehaves, print messages as JSON after each step. The two common failure modes:

  • Schema drift: The model passes args with extra keys. json.loads ignores them, but your handler must default missing values.
  • Silent hallucination: The model claims a file exists without reading it. The system prompt rule “do not guess” mitigates this, but you can also assert that read_file was called before write_file on the same path in your orchestrator.

Hardening for real use

The script above is a starting point, not a product. Concrete improvements we’ve shipped:

  • Command allowlist. Parse cmd and reject anything not in a small set (pytest, python, ls, cat). The model rarely needs more for code tasks.
  • Diff-based writes. Instead of full file overwrite, have the agent emit unified diffs and apply with patch. This limits blast radius and makes reviews trivial.
  • Token accounting. Gateways meter per-token usage; log resp.usage each step so you can attribute cost per task. A long agent loop can silently burn tokens on repeated reads.
  • System prompt caching. If your gateway forwards provider cache-control hints, mark the static system prompt and tool schema as cached to cut latency and cost on long loops.
  • Deterministic stop. Never let the loop run unbounded. A max_steps counter is the cheapest reliability win.

Why this shape works

Claude Opus 4.8 handles multi-step tool sequences well when each step’s result is fed back verbatim. The failure mode is silent hallucination of file state—hence the strict “no guessing, always read” rule in the system prompt. Keeping tools side-effect-free except inside the sandbox makes the agent debuggable: every action is a line in the transcript.

If you need to scale this beyond a single process, move tool execution behind a queue and keep the model loop single-threaded. The model is the bottleneck, not your shell.

That’s the whole machine: a tight loop, three tools, and an OpenAI-compatible client pointed at Claude Opus 4.8. Extend the tool schema before you extend the prompt.

Tagsclaude-opus-4-8coding-agentstutorialagentic-coding

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All claude opus 4.8 for agentic coding posts →