> ## Documentation Index
> Fetch the complete documentation index at: https://docs.opper.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Claude Code clone

> Build a tiny coding agent that reads, writes, and runs code in a loop, using the gateway and tool calling.

A coding agent is just a tool-calling loop with the right tools. Give the model `read_file`, `write_file`, and `run_command`, then keep calling it: as long as it returns tool calls, you run them and feed the results back. It writes code, runs it, reads the error, fixes it, and goes again until the task is done. Conversation state persists, so follow-up requests build on what it already did.

<div className="opp-term" style={{ borderRadius: "12px", overflow: "hidden", border: "1px solid #232a33", fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: "13.5px", lineHeight: "1.65", boxShadow: "0 8px 30px rgba(0,0,0,0.35)", margin: "1.5em 0" }}>
  <div className="opp-head" style={{ display: "flex", alignItems: "center", gap: "7px", padding: "11px 14px", background: "#11161c", borderBottom: "1px solid #232a33" }}>
    <span style={{ width: "11px", height: "11px", borderRadius: "50%", background: "#ff5f56", display: "inline-block" }} />

    <span style={{ width: "11px", height: "11px", borderRadius: "50%", background: "#ffbd2e", display: "inline-block" }} />

    <span style={{ width: "11px", height: "11px", borderRadius: "50%", background: "#27c93f", display: "inline-block" }} />

    <span style={{ marginLeft: "10px", color: "#6e7681", fontSize: "12px" }}>python app.py</span>
  </div>

  <div className="opp-body" style={{ padding: "16px 18px", background: "#0b0f14", color: "#c9d1d9" }}>
    <p data-role="user" style={{ margin: "0 0 16px" }}><span style={{ color: "#58a6ff", fontWeight: 600 }}>You ›</span> Build a fizzbuzz script and run it for n=15.</p>
    <p data-role="tool" style={{ margin: "0 0 16px" }}><span style={{ color: "#e3b341", fontWeight: 600 }}>Tool ›</span> write\_file("fizzbuzz.py")<br /><span style={{ color: "#6e7681" }}>→ Wrote 545 bytes</span></p>
    <p data-role="tool" style={{ margin: "0 0 16px" }}><span style={{ color: "#e3b341", fontWeight: 600 }}>Tool ›</span> run\_command("python fizzbuzz.py")<br /><span style={{ color: "#e0786e" }}>→ /bin/sh: python: command not found</span></p>
    <p data-role="tool" style={{ margin: "0 0 16px" }}><span style={{ color: "#e3b341", fontWeight: 600 }}>Tool ›</span> run\_command("python3 fizzbuzz.py")<br /><span style={{ color: "#6e7681" }}>→ 1 · 2 · Fizz · 4 · Buzz · Fizz · 7 · 8 · Fizz · Buzz · 11 · Fizz · 13 · 14 · FizzBuzz</span></p>
    <p data-role="bot" style={{ margin: "0 0 16px" }}><span style={{ color: "#14cdcd", fontWeight: 600 }}>Bot ›</span> Done. Wrote fizzbuzz.py, recovered from the missing `python`, and verified the output for n=15.</p>
    <p data-role="user" style={{ margin: "0 0 16px" }}><span style={{ color: "#58a6ff", fontWeight: 600 }}>You ›</span> Now add a test that fizzbuzz(5) ends with "Buzz", and run it.</p>
    <p data-role="tool" style={{ margin: "0 0 16px" }}><span style={{ color: "#e3b341", fontWeight: 600 }}>Tool ›</span> read\_file("fizzbuzz.py")<br /><span style={{ color: "#6e7681" }}>→ def fizzbuzz(n): ...</span></p>
    <p data-role="tool" style={{ margin: "0 0 16px" }}><span style={{ color: "#e3b341", fontWeight: 600 }}>Tool ›</span> write\_file("test\_fizzbuzz.py")<br /><span style={{ color: "#6e7681" }}>→ Wrote 184 bytes</span></p>
    <p data-role="tool" style={{ margin: "0 0 16px" }}><span style={{ color: "#e3b341", fontWeight: 600 }}>Tool ›</span> run\_command("python3 -m unittest -v")<br /><span style={{ color: "#6e7681" }}>→ Ran 1 test · OK</span></p>
    <p data-role="bot" style={{ margin: 0 }}><span style={{ color: "#14cdcd", fontWeight: 600 }}>Bot ›</span> Added test\_fizzbuzz.py and it passes. fizzbuzz(5) ends with "Buzz".<span className="opp-cursor" /></p>
  </div>
</div>

Notice the loop adapting: `python` wasn't found, so it retried with `python3`; on the follow-up it read the existing file before writing the test. You don't script either move, the model reacts to what the tools return.

## The agent

<CodeGroup>
  ```python app.py theme={null}
  import os, json, subprocess
  from openai import OpenAI

  client = OpenAI(base_url="https://api.opper.ai/v3/compat", api_key=os.environ["OPPER_API_KEY"])
  WORKDIR = "workspace"
  os.makedirs(WORKDIR, exist_ok=True)

  def read_file(path):
      p = os.path.join(WORKDIR, path)
      return open(p).read() if os.path.exists(p) else f"(no file at {path})"

  def write_file(path, content):
      with open(os.path.join(WORKDIR, path), "w") as f:
          f.write(content)
      return f"Wrote {len(content)} bytes to {path}"

  def run_command(command):
      r = subprocess.run(command, shell=True, cwd=WORKDIR, capture_output=True, text=True, timeout=30)
      return (r.stdout + r.stderr).strip() or "(no output)"

  TOOLS = {"read_file": read_file, "write_file": write_file, "run_command": run_command}

  specs = [
      {"type": "function", "function": {"name": "read_file", "description": "Read a file in the workspace.",
          "parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}},
      {"type": "function", "function": {"name": "write_file", "description": "Create or overwrite a file in the workspace.",
          "parameters": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}},
      {"type": "function", "function": {"name": "run_command", "description": "Run a shell command in the workspace and return its output.",
          "parameters": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}},
  ]

  SYSTEM = (
      "You are a coding agent. Use the tools to read, write, and run code in the "
      "workspace until the task is done. Verify your work, then give a short summary."
  )

  messages = [{"role": "system", "content": SYSTEM}]

  def send(user_message):
      messages.append({"role": "user", "content": user_message})
      # Keep going while the model wants to call tools.
      while True:
          r = client.chat.completions.create(model="anthropic/claude-sonnet-4-6", messages=messages, tools=specs)
          msg = r.choices[0].message
          messages.append(msg)
          if not msg.tool_calls:
              return msg.content
          for call in msg.tool_calls:
              args = json.loads(call.function.arguments)
              print(f"  · {call.function.name}")
              messages.append({"role": "tool", "tool_call_id": call.id,
                               "content": TOOLS[call.function.name](**args)})

  print(send("Write fizzbuzz.py with a fizzbuzz(n) function, then run it for n=15."))
  print(send("Now add a test that fizzbuzz(5) ends with 'Buzz', and run it."))
  ```

  ```typescript app.ts theme={null}
  import OpenAI from "openai";
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
  import { execSync } from "node:child_process";
  import path from "node:path";

  const client = new OpenAI({ baseURL: "https://api.opper.ai/v3/compat", apiKey: process.env.OPPER_API_KEY });
  const WORKDIR = "workspace";
  if (!existsSync(WORKDIR)) mkdirSync(WORKDIR);

  const TOOLS: Record<string, (args: any) => string> = {
    read_file: ({ path: p }) =>
      existsSync(path.join(WORKDIR, p)) ? readFileSync(path.join(WORKDIR, p), "utf8") : `(no file at ${p})`,
    write_file: ({ path: p, content }) => {
      writeFileSync(path.join(WORKDIR, p), content);
      return `Wrote ${content.length} bytes to ${p}`;
    },
    run_command: ({ command }) => {
      try {
        return execSync(command, { cwd: WORKDIR, timeout: 30000 }).toString().trim() || "(no output)";
      } catch (e: any) {
        return ((e.stdout?.toString() || "") + (e.stderr?.toString() || "")).trim() || String(e);
      }
    },
  };

  const specs = [
    { type: "function" as const, function: { name: "read_file", description: "Read a file in the workspace.", parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } } },
    { type: "function" as const, function: { name: "write_file", description: "Create or overwrite a file in the workspace.", parameters: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] } } },
    { type: "function" as const, function: { name: "run_command", description: "Run a shell command in the workspace and return its output.", parameters: { type: "object", properties: { command: { type: "string" } }, required: ["command"] } } },
  ];

  const SYSTEM =
    "You are a coding agent. Use the tools to read, write, and run code in the " +
    "workspace until the task is done. Verify your work, then give a short summary.";

  const messages: any[] = [{ role: "system", content: SYSTEM }];

  async function send(userMessage: string) {
    messages.push({ role: "user", content: userMessage });
    // Keep going while the model wants to call tools.
    while (true) {
      const r = await client.chat.completions.create({ model: "anthropic/claude-sonnet-4-6", messages, tools: specs });
      const msg = r.choices[0].message;
      messages.push(msg);
      if (!msg.tool_calls) return msg.content;
      for (const call of msg.tool_calls) {
        const args = JSON.parse(call.function.arguments);
        console.log(`  · ${call.function.name}`);
        messages.push({ role: "tool", tool_call_id: call.id, content: TOOLS[call.function.name](args) });
      }
    }
  }

  console.log(await send("Write fizzbuzz.js with a fizzbuzz(n) function, then run it with node for n=15."));
  console.log(await send("Now add a test that fizzbuzz(5) ends with 'Buzz', and run it with node."));
  ```
</CodeGroup>

Run it:

<CodeGroup>
  ```bash Python theme={null}
  pip install openai
  export OPPER_API_KEY="your-api-key"
  python app.py
  ```

  ```bash TypeScript theme={null}
  npm install openai
  export OPPER_API_KEY="your-api-key"
  npx tsx app.ts
  ```
</CodeGroup>

## How it works

* **The model drives the loop.** Each turn it either calls tools or returns a final answer. You run the tools, feed the results back, and loop *while it keeps calling tools*, no fixed step count.
* **It recovers on its own.** Tool output (including errors) goes straight back to the model, so it adapts: a failed command, a syntax error, a missing file. You don't script the recovery.
* **State persists.** `messages` holds the whole history, so a follow-up like "now add a test" builds on the code the agent already wrote.
* **Three tools go a long way.** `read_file`, `write_file`, and `run_command` are enough to edit and test a project. Add more (search, git, HTTP) and the agent gets more capable. Swap `model` for any of the [300+ models](/capabilities/models) without touching the loop.

<Tip>
  `run_command` executes shell commands, so sandbox it before pointing it at anything real: a container, a throwaway directory, or an allowlist of commands. The example keeps everything inside a `workspace` folder.
</Tip>

## What's next

<CardGroup cols={2}>
  <Card title="Tool calling" icon="screwdriver-wrench" href="/build/gateway/tools">
    The tool-use round trip this loop is built on.
  </Card>

  <Card title="Agents SDK" icon="robot" href="/agents/overview">
    A batteries-included agent framework with tools, streaming, and multi-agent.
  </Card>

  <Card title="Conversations" icon="comments" href="/build/gateway/conversations">
    Message history and multi-turn patterns.
  </Card>

  <Card title="Observe" icon="eye" href="/control-plane/observe">
    Score the agent's runs and catch regressions.
  </Card>
</CardGroup>
