How to build your own Agentic Code Editor

Sep 11, 2025

How to build your own Agentic Code Editor (Step by Step)



Build a tiny terminal coding agent in Python that chats with Claude and can read, list, and edit files. It’s fast to set up and easy to extend. You’ll go from zero to working in minutes.


What you will build

- A minimal chatbot with Anthropic’s Claude main.py

- A basic agent loop that lets Claude edit code locally. run.py

- We will use three tools: read_file, list_files, and edit_file.


Prerequisites

export ANTHROPIC_API_KEY=you_need_a_key
uv init indubitably-code
cd indubitably-code
uv venv
source


Step 1: Minimal terminal chatbot

# main.py
import os
import sys
from anthropic import Anthropic

def main():
    client = Anthropic()  # reads ANTHROPIC_API_KEY
    conversation = []     # list[dict]: [{"role":"user"/"assistant","content":[...]}]

    print("Chat with Claude (ctrl-c to quit)")
    for line in sys.stdin:
        user_text = line.rstrip("\n")
        conversation.append({"role": "user", "content": [{"type": "text", "text": user_text}]})

        msg = client.messages.create(
            model="claude-3-7-sonnet-latest",
            max_tokens=1024,
            messages=conversation,
        )
        # append assistant turn to keep stateless API “stateful”
        conversation.append({"role": "assistant", "content": msg.content})

        # print any text blocks
        for block in msg.content:
            if block.type == "text":
                print(f"Claude: {block.text}")

if __name__ == "__main__":
    main()


The Anthropic messages API is stateless. This loop keeps your own conversation, sends it each turn, and prints text blocks. We will build on this basic chatbot and extend it.


Step 2: Add tool plumbing (registry + execution loop)


We add a tiny tool registry and a loop that detects tool_use blocks, runs the tool locally, and replies with tool_result blocks. This mirrors the standard pattern in Anthropic’s docs.

# agent.py
import json
import sys
from typing import Callable, Dict, Any, List
from anthropic import Anthropic

ToolFunc = Callable[[Dict[str, Any]], str]

class Tool:
    def __init__(self, name: str, description: str, input_schema: Dict[str, Any], fn: ToolFunc):
        self.name = name
        self.description = description
        self.input_schema = input_schema
        self.fn = fn

def run_agent(tools: List[Tool]):
    client = Anthropic()
    conversation: List[Dict[str, Any]] = []

    print("Chat with Claude (ctrl-c to quit)")
    read_user = True

    while True:
        if read_user:
            line = sys.stdin.readline()
            if not line:
                break
            conversation.append({"role": "user", "content": [{"type": "text", "text": line.rstrip('\n')}]})

        # prepare Anthropic tool definitions
        tool_defs = [{
            "name": t.name,
            "description": t.description,
            "input_schema": t.input_schema,
        } for t in tools]

        msg = client.messages.create(
            model="claude-3-7-sonnet-latest",
            max_tokens=1024,
            messages=conversation,
            tools=tool_defs,  # enabling tool use
        )

        conversation.append({"role": "assistant", "content": msg.content})

        tool_results_content: List[Dict[str, Any]] = []
        for block in msg.content:
            if block.type == "text":
                print(f"Claude: {block.text}")
            elif block.type == "tool_use":
                tool_name = block.name
                tool_input = block.input  # already a dict
                tool_use_id = block.id

                # dispatch
                impl = next((t for t in tools if t.name == tool_name), None)
                if impl is None:
                    tool_results_content.append({
                        "type": "tool_result",
                        "tool_use_id": tool_use_id,
                        "content": "tool not found",
                        "is_error": True,
                    })
                else:
                    try:
                        result_str = impl.fn(tool_input)
                        tool_results_content.append({
                            "type": "tool_result",
                            "tool_use_id": tool_use_id,
                            "content": result_str,
                            "is_error": False,
                        })
                    except Exception as e:
                        tool_results_content.append({
                            "type": "tool_result",
                            "tool_use_id": tool_use_id,
                            "content": str(e),
                            "is_error": True,
                        })

        if not tool_results_content:
            read_user = True
            continue

        # send tool results back as a user message and loop again
        conversation.append({"role": "user", "content": tool_results_content})
        read_user = False


The model emits tool_use. You reply with tool_result blocks in a user message, then call the API again. It's a while loop under the hood you execute the tools that the model is responding back to use. They are eager to use tools as their training datasets produce a model that "knows" how to use tools.


Step 3: Add the read_file tool

# tools_read.py
import os
from typing import Dict, Any

def read_file_tool_def():
    return {
        "name": "read_file",
        "description": "Read the contents of a given relative file path. Use for file contents. Not for directories.",
        "input_schema": {
            "type": "object",
            "additionalProperties": False,
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Relative path to a file in the working directory."
                }
            },
            "required": ["path"]
        }
    }

def read_file_impl(input: Dict[str, Any]) -> str:
    path = input.get("path", "")
    if not path:
        raise ValueError("missing 'path'")
    with open(path, "r", encoding="utf-8") as f:
        return f.read()


Step 4: Add the list_files tool


This is recursive and adds a trailing / for directories.

# tools_list.py
import os
from typing import Dict, Any, List

def list_files_tool_def():
    return {
        "name": "list_files",
        "description": "List files and directories at a given path. If path omitted, list current directory.",
        "input_schema": {
            "type": "object",
            "additionalProperties": False,
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Optional relative path. Defaults to current directory."
                }
            }
        }
    }

def list_files_impl(input: Dict[str, Any]) -> str:
    start = input.get("path") or "."
    results: List[str] = []
    for root, dirs, files in os.walk(start):
        # make root relative to 'start'
        rel_root = os.path.relpath(root, start)
        def append_entry(name, is_dir):
            rel_path = name if rel_root == "." else os.path.join(rel_root, name)
            results.append(rel_path + ("/" if is_dir else ""))
        for d in dirs:
            append_entry(d, True)
        for f in files:
            append_entry(f, False)
    import json
    return json.dumps(results)


Step 5: Add the edit_file tool (replace or create)

# tools_edit.py
import os
from pathlib import Path
from typing import Dict, Any

def edit_file_tool_def():
    return {
        "name": "edit_file",
        "description": (
            "Make edits to a text file.\n"
            "Replaces 'old_str' with 'new_str' in the given file. "
            "'old_str' and 'new_str' MUST be different. "
            "If the file does not exist and old_str == '', the file is created with new_str."
        ),
        "input_schema": {
            "type": "object",
            "additionalProperties": False,
            "properties": {
                "path": {"type": "string", "description": "The path to the file"},
                "old_str": {"type": "string", "description": "Exact text to replace (must match exactly)"},
                "new_str": {"type": "string", "description": "Replacement text"}
            },
            "required": ["path", "old_str", "new_str"]
        }
    }

def _create_new_file(file_path: str, content: str) -> str:
    p = Path(file_path)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(content, encoding="utf-8")
    return f"Successfully created file {file_path}"

def edit_file_impl(input: Dict[str, Any]) -> str:
    path = input.get("path", "")
    old = input.get("old_str", None)
    new = input.get("new_str", None)

    if not path or old is None or new is None or old == new:
        raise ValueError("invalid input parameters")

    if not os.path.exists(path):
        if old == "":
            return _create_new_file(path, new)
        raise FileNotFoundError(path)

    content = Path(path).read_text(encoding="utf-8")
    if old == "":
        # create-only behavior when old == ""
        Path(path).write_text(new, encoding="utf-8")
        return "OK"

    new_content = content.replace(old, new)
    if new_content == content:
        raise ValueError("old_str not found in file")

    Path(path).write_text(new_content, encoding="utf-8")
    return "OK"


Step 6: Wire it up (add tools to the loop)

# run.py
from agent import run_agent, Tool
from tools_read import read_file_tool_def, read_file_impl
from tools_list import list_files_tool_def, list_files_impl
from tools_edit import edit_file_tool_def, edit_file_impl

if __name__ == "__main__":
    tools = [
        Tool(**read_file_tool_def(), fn=read_file_impl),
        Tool(**list_files_tool_def(), fn=list_files_impl),
        Tool(**edit_file_tool_def(), fn=edit_file_impl),
    ]
    run_agent(tools)


Ready to have your mind blown? Try it out

uv run run.py
# then try:
# what's in main.py?
# what do you see in this directory?
# hey claude, create fizzbuzz.js that I can run with Nodejs and that has fizzbuzz in it and executes it
# Please edit fizzbuzz.js so that it only prints until 15


Compare it to a chatbot without agent tools

uv run main.py
# Classic chatbot in the terminal


Want to learn more and build tools for your business together? Contact us today!