How to build your own Agentic Code Editor
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
sourceStep 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 = FalseThe 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 15Compare it to a chatbot without agent tools
uv run main.py
# Classic chatbot in the terminalWant to learn more and build tools for your business together? Contact us today!
Found this helpful? Share it!