How to Build Your First Coding Agent From Scratch
Learn to build a coding agent with a minimal loop, tools, and permissions. Includes runnable code and CLI examples.
Before you start
You have an idea for a coding agent that can modify code, run tests, and fix issues. But you are not sure where to start. Many tutorials assume you have a complex framework, but you can build a functional agent with a few hundred lines of code.
This guide walks you through building a minimal coding agent from scratch. You will create an agent loop, define tools, set up permissions, and run it on a sample task. By the end, you will have a working agent you can extend.
You need Node.js 18 or later, an OpenAI API key (or any compatible API), and a basic understanding of TypeScript. We will use the OpenAI SDK, but the pattern works with any LLM provider.
node --version
npm init -y
npm install openai dotenv- Node.js 18+ installed
- An OpenAI API key (or other LLM API key)
- A code editor and terminal
- Basic knowledge of TypeScript and shell commands
Step 1: Set up the project structure
Create a project directory and set up a simple structure. We will have a main entry point, a tools module, and a permissions file. This keeps the code clean and easy to extend.
mkdir coding-agent
cd coding-agent
mkdir src
mkdir tools
mkdir permissions
cat > .env << 'EOF'
OPENAI_API_KEY=your-api-key-here
EOFStep 2: Define the agent loop
The core of a coding agent is a loop: the LLM decides which tool to call, the agent executes it, and the result is fed back. We will implement this loop in a single file using the OpenAI chat completions API.
The loop runs until the model signals it is done. We use a special tool called 'finish' to stop the loop and return the final answer.
// src/agent.ts
import OpenAI from 'openai';
import { executeTool } from './tools';
const openai = new OpenAI();
export async function runAgent(task: string) {
const messages: any[] = [{ role: 'user', content: task }];
const tools = [
{
type: 'function',
function: {
name: 'run_command',
description: 'Run a shell command',
parameters: {
type: 'object',
properties: {
command: { type: 'string', description: 'The command to run' }
},
required: ['command']
}
}
},
{
type: 'function',
function: {
name: 'read_file',
description: 'Read a file from disk',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Path to the file' }
},
required: ['path']
}
}
},
{
type: 'function',
function: {
name: 'write_file',
description: 'Write content to a file',
parameters: {
type: 'object',
properties: {
path: { type: 'string' },
content: { type: 'string' }
},
required: ['path', 'content']
}
}
},
{
type: 'function',
function: {
name: 'finish',
description: 'Finish the task',
parameters: {
type: 'object',
properties: {
answer: { type: 'string', description: 'Final answer' }
},
required: ['answer']
}
}
}
];
while (true) {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages,
tools
});
const message = response.choices[0].message;
messages.push(message);
if (message.tool_calls) {
for (const call of message.tool_calls) {
const result = await executeTool(call.function.name, JSON.parse(call.function.arguments));
messages.push({
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify(result)
});
}
} else {
// No tool calls means we are done
return message.content;
}
}
}Step 3: Implement the tools
Tools are the agent's hands. We will implement run_command, read_file, write_file, and finish. Each tool returns a JSON object that gets sent back to the model.
The run_command tool is powerful, so we will add a permission check later. For now, we just execute the command with child_process.
// src/tools.ts
import { execSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';
import { checkPermission } from './permissions';
export async function executeTool(name: string, args: any) {
switch (name) {
case 'run_command':
if (!checkPermission('run_command', args.command)) {
return { error: 'Permission denied' };
}
try {
const output = execSync(args.command, { encoding: 'utf-8', shell: '/bin/bash' });
return { stdout: output };
} catch (e: any) {
return { error: e.stderr || e.message };
}
case 'read_file':
try {
const content = readFileSync(args.path, 'utf-8');
return { content };
} catch (e: any) {
return { error: e.message };
}
case 'write_file':
try {
writeFileSync(args.path, args.content);
return { success: true };
} catch (e: any) {
return { error: e.message };
}
case 'finish':
return { answer: args.answer };
default:
return { error: 'Unknown tool' };
}
}Step 4: Add permissions and safety
A coding agent can run arbitrary commands, so you need a permission layer. We will create a simple allowlist of commands that are safe to run automatically. Anything else will require manual approval.
In a real agent, you would also restrict file paths and use a sandbox. This minimal version shows the concept.
// src/permissions.ts
const allowedCommands = ['ls', 'cat', 'grep', 'node', 'npm test'];
export function checkPermission(toolName: string, args: any): boolean {
if (toolName === 'run_command') {
const command = args.command;
// Allow only if command starts with an allowed prefix
return allowedCommands.some((prefix) => command.startsWith(prefix));
}
return true;
}Step 5: Run the agent on a sample task
Now we can test the agent. We will give it a task like 'List files in the current directory and then finish.' The agent will call the run_command tool with 'ls' and then finish.
// src/index.ts
import { runAgent } from './agent';
const task = 'List files in the current directory and then finish.';
runAgent(task).then((result) => {
console.log('Final answer:', result);
});Step 6: Verify it worked
Run the agent with the following command. You should see the agent list files and then print a final answer. If you get an error, check your API key and network connection.
npx tsx src/index.ts- Expected output: the agent prints a list of files and a final answer.
- If you see a permission error, adjust the allowed commands list.
- If the model is not calling tools, check that the tool schema is correct.
Step 7: Extend with more tools
A real coding agent needs more tools: edit files, run tests, search code, and maybe interact with git. You can add these by defining new functions and implementing them in the tools module.
Here is an example of adding a 'search_code' tool that uses grep to find patterns.
// In tools.ts, add to the switch:
case 'search_code':
try {
const output = execSync(`grep -rn ${JSON.stringify(args.pattern)} ${args.path}`, { encoding: 'utf-8', shell: '/bin/bash' });
return { matches: output };
} catch (e: any) {
return { error: e.stderr || e.message };
}
// In agent.ts, add to the tools array:
{
type: 'function',
function: {
name: 'search_code',
description: 'Search for a pattern in code',
parameters: {
type: 'object',
properties: {
pattern: { type: 'string' },
path: { type: 'string' }
},
required: ['pattern', 'path']
}
}
}Recommended setup for a real project
For a production coding agent, you need more robust safety, better tool design, and observability. Here is what I would do:
Use a sandboxed environment like Docker or a VM. Never run the agent directly on your host with broad permissions.
Implement a structured permission system with a config file that lists allowed commands and paths. Use a human-in-the-loop for destructive actions.
Add logging and tracing so you can see every tool call and its result. This is crucial for debugging.
Use a more capable model like GPT-4 or Claude for complex tasks, but note the cost.
# permissions.yaml
allowed_commands:
- ls
- cat
- grep
- node
- npm test
- git status
- git diff
allowed_paths:
- /home/user/project
blocked_commands:
- rm -rf
- sudo
- mkfsTroubleshooting
If the agent does not work, here are common issues and fixes.
- API key not set: make sure the OPENAI_API_KEY environment variable is set.
- Model not calling tools: check the tool schema and ensure the model is instructed to use tools.
- Permission denied: adjust the allowed commands list.
- Command not found: use absolute paths or update the PATH in the shell.
- Rate limiting: add retries or use a different model.
FAQ
Answers to the questions that come up most often on this topic.
- Q: Can I use a local LLM instead of OpenAI? A: Yes, you can use any OpenAI-compatible API. Set the base URL to your local server.
- Q: How do I prevent the agent from doing damage? A: Use strict permissions, sandboxing, and human approval for risky commands.
- Q: What is the best model for coding agents? A: It depends on your task. GPT-4o and Claude 3.5 are strong, but smaller models like GPT-4o-mini are cheaper and faster.
- Q: How do I add more tools? A: Define the function schema and implement the execution logic in the tools module.
- Q: Can I run the agent on a CI pipeline? A: Yes, but add extra safety and logging.
Next steps
Now that you have a working agent, try it on a real task: have it read a file, make a change, and run tests. Start with a simple project and expand from there.
Run the following command to test your agent on a file modification task.
echo 'console.log("Hello")' > sample.js
npx tsx src/index.ts 'Read sample.js, change Hello to Hi, and write it back'Key takeaways
- Apply one concrete change from this post before collecting more reading.
- Prefer browser-side tools when the work involves secrets, tokens, or PII.
- Document the why next to the how so the next reviewer inherits context.
FAQ
- Who is this guide on ai-agents for?
- Working developers who need a practical take on how to build your first coding agent from scratch — not a marketing overview. Skim the sections, apply one tip, then come back when you hit an edge case.
- Do I need an account to use the related tools?
- No. code.live tools run in your browser with no signup. Nothing you paste is uploaded to a server for the client-side utilities linked from this post.
- How often is this article updated?
- This post was published August 27, 2026. Fundamentals stay stable; check linked tool pages and official docs when version-specific behavior matters.