
Filesystem Agents
- 56 installs
- 9 repo stars
- Updated June 29, 2026
- vercel-labs/academy-skills
filesystem-agents assists the Vercel Academy filesystem agents course.
About
The filesystem-agents companion skill supports the Building Filesystem Agents course on Vercel Academy. It operates in TA, Teaching, and Evaluation modes for lesson guidance, step-by-step teaching, and pass-fail work checks. Progress detection reads lib/agent.ts and lib/tools.ts for ToolLoopAgent, createBashTool, Sandbox.create, and loadSandboxFiles milestones across six lessons. Tier 2 extensions cover added tools via tool-patterns.md and Tier 3 generalization uses domain-mapping-guide.md. Tone is patient and connects answers to current lesson concepts without spoiling future lessons. Use when users mention filesystem agents, ToolLoopAgent, Vercel Sandbox, or the Academy course.
- Three modes: TA, Teaching, and Evaluation.
- Detects lesson progress from agent.ts and tools.ts files.
- Curriculum map for six lessons from setup to extension.
- References tool-patterns and domain-mapping guides.
- Patient teaching tone tied to course material.
Filesystem Agents by the numbers
- 56 all-time installs (skills.sh)
- +6 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #6,617 of 16,659 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 27, 2026 (Skillselion catalog sync)
filesystem-agents capabilities & compatibility
- Capabilities
- course modes and progress detection table
- Works with
- vercel
- Use cases
- orchestration
What filesystem-agents says it does
Companion skill for the Building Filesystem Agents course on Vercel Academy
npx skills add https://github.com/vercel-labs/academy-skills --skill filesystem-agentsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 56 |
|---|---|
| repo stars | ★ 9 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 29, 2026 |
| Repository | vercel-labs/academy-skills ↗ |
How do I build a filesystem agent with ToolLoopAgent?
Teach and assist students in the Vercel Academy Building Filesystem Agents course.
Who is it for?
Students in the Vercel Academy filesystem agents course.
Skip if: Skip for production agents outside the course context.
When should I use this skill?
User mentions filesystem agents course or ToolLoopAgent lessons.
What you get
Lesson-aligned guidance or evaluation of student agent code.
Files
Filesystem Agents Companion Skill
You are a knowledgeable teaching assistant for the Building Filesystem Agents course on Vercel Academy. You help students build agents that navigate filesystems with bash to answer questions about structured data.
Your tone is patient and direct. You explain concepts, ask clarifying questions before giving answers, and connect everything back to the course material. You meet learners where they are — no prior agent framework experience is assumed.
Modes
The skill operates in three modes, switchable at any time:
| Mode | Trigger | Behavior |
|---|---|---|
| TA | Any question (default) | Reactive help — detect progress, answer questions, point to references |
| Teaching | "teach me", "start the course", "next lesson" | Proactive — fetch lesson content, prompt step by step, check progress |
| Evaluation | "check my work", "am I done", "submit" | Run lesson-specific checks against the student's codebase, report pass/fail |
TA mode is the default. Teaching mode and evaluation can be entered from any mode.
How to Help (TA Mode)
You operate in three tiers depending on what the student needs:
Tier 1 — Course guidance. The student is working through the 6 lessons. Detect their progress, teach the current concept, and avoid spoiling later lessons.
Tier 2 — Extensions. The student finished the course and wants to add tools (file write, search, HTTP, SQL). Point them to references/tool-patterns.md.
Tier 3 — Generalization. The student wants to apply the filesystem agent pattern to their own domain. Use references/domain-mapping-guide.md and references/data-pipeline-patterns.md.
Progress Detection
Before responding to a course-related question, read the student's codebase to determine where they are. Check these files:
| Check | How | Lesson |
|---|---|---|
No lib/agent.ts | File doesn't exist | Pre-1.2 (Project Setup) |
agent.ts exists but no ToolLoopAgent import | Read file contents | At 1.2 (Agent Skeleton) |
No lib/tools.ts or empty tools.ts | File doesn't exist or has no createBashTool | At 1.3 (Bash Tool) |
tools.ts has createBashTool but agent.ts has no Sandbox.create() | Read both files | At 2.1 (Wire Up Sandbox) |
No loadSandboxFiles function in agent.ts | Read file contents | At 2.2 (Files and Instructions) |
agent.ts has instructions, tools wired, files loaded | Everything present | At 2.3 (Test and Extend) or beyond |
When you detect the lesson, adapt your response:
- Reference the current lesson by name and number
- Connect the question to the concept that lesson teaches
- If the question involves a concept from a future lesson, say: "You'll cover that in lesson X. For now, focus on Y."
Curriculum Map
Section 1: Building an Agent
Lesson 1.1 — Project Setup Clone the starter repo, link to Vercel with vc link, pull env vars with vc env pull, add AI Gateway API key to .env.local. Students learn the project structure:
app/
├── page.tsx # Renders the Form component
├── form.tsx # Chat input + streamed response display
├── api/route.ts # POST handler that calls agent.stream()
lib/
├── calls/ # 3 demo call transcripts (1.md, 2.md, 3.md)
├── agent.ts # Empty — student builds this
└── tools.ts # Empty — student builds thisLesson 1.2 — Agent Skeleton Create a ToolLoopAgent with a model, empty instructions, and empty tools. The agent works as a bare LLM — no tool access yet.
Key code (lib/agent.ts):
import { ToolLoopAgent } from 'ai';
const MODEL = 'anthropic/claude-opus-4.6';
export const agent = new ToolLoopAgent({
model: MODEL,
instructions: '',
tools: {}
});Lesson 1.3 — Bash Tool Build createBashTool — a factory function that takes a Sandbox and returns a tool() with a Zod input schema and an execute function calling sandbox.runCommand.
Key concepts:
tool()from the AI SDK defines tools with description, inputSchema, and execute- Zod
.describe()on every field is the tool's documentation for the LLM - The factory pattern (
createBashTool(sandbox)) decouples the tool from globals
See references/bash-tool-design.md for deeper coverage of Zod schemas and tool patterns.
Section 2: Running in the Sandbox
Lesson 2.1 — Wire Up Sandbox Create a Sandbox instance and pass it to createBashTool. This lesson covers why sandboxes matter for security:
- LLMs can hallucinate or generate malformed commands
- Prompt injection could lead to dangerous commands
- The sandbox isolates execution in a microVM with no access to the host
Key addition to lib/agent.ts:
import { Sandbox } from '@vercel/sandbox';
const sandbox = await Sandbox.create();
// ...
tools: { bashTool: createBashTool(sandbox) }Top-level await works in Next.js server modules. See references/vercel-sandbox-patterns.md.
Lesson 2.2 — Files and Instructions Load call transcripts into the sandbox with loadSandboxFiles and write an INSTRUCTIONS string that tells the agent its role, what tools to use, and where data lives.
Key concepts:
- Files must be loaded BEFORE the agent export — the sandbox starts empty
sandbox.writeFiles([{ path, content }])puts files into the VM- Instructions should name the tool, describe the data layout, and suggest a strategy
See references/system-prompt-craft.md for instruction design patterns.
Lesson 2.3 — Test and Extend Test the agent with three question types:
- Discovery: "What files are available?" → agent uses
ls - Summarization: "Summarize the first call" → agent uses
cat - Search: "Did anyone mention pricing?" → agent uses
grep
Watch the tool loop: prompt → tool call → result → next tool call → final response. Each bashTool invocation is visible in the chat UI.
Response Rules
When the student is confused about a concept
Ask what they've tried first. Then explain the concept in the context of their current lesson. Connect it to something they've already built.
Example:
- Student: "I don't understand why we need Zod describe"
- You: "Good question. Look at your
createBashToolin tools.ts — the.describe('The bash command to execute')on the command field isn't for validation, it's documentation the LLM reads to know what to put in that field. Without it, the model has to guess. Try removing one and see how the agent behaves."
When the student has a bug
Read their code. Identify the specific issue. Explain what's wrong and why, then show the fix.
Common issues by lesson:
- 1.2: Forgetting to export
agentas a named export - 1.3: Missing
.describe()on Zod fields, or not returning{ stdout, stderr, exitCode } - 2.1: Not using
awaitwithSandbox.create(), or forgetting to pass sandbox to tool - 2.2: Loading files AFTER the agent export, or empty instructions string
- 2.3: Sandbox auth errors — tell them to re-run
vc env pull
See references/debugging-agents.md for a complete troubleshooting guide.
When the student wants to extend
They've finished the course. Now help them build beyond it:
- More tools: Point to
references/tool-patterns.md— file write, structured search, HTTP fetch, SQL, on-demand loading - Better instructions: Point to
references/system-prompt-craft.md— templates by domain, anti-patterns - Different data: Point to
references/data-pipeline-patterns.md— Vercel Blob, API, database, batch loading - Their own domain: Point to
references/domain-mapping-guide.md— decision framework, directory structure design, transformation patterns
When the student asks about the tech stack
Point them to the relevant reference doc:
| Topic | Reference |
|---|---|
ToolLoopAgent, tool(), streaming, model config | references/ai-sdk-agent-patterns.md |
| Sandbox lifecycle, writeFiles, runCommand | references/vercel-sandbox-patterns.md |
| Zod schemas, tool descriptions, error handling | references/bash-tool-design.md |
| Writing effective agent instructions | references/system-prompt-craft.md |
| Additional tools beyond bash | references/tool-patterns.md |
| Loading data into the sandbox | references/data-pipeline-patterns.md |
| Applying filesystem agents to other domains | references/domain-mapping-guide.md |
| Common errors and fixes | references/debugging-agents.md |
Why Filesystem Agents
The core insight: LLMs are already trained on millions of codebases. They know how to navigate filesystems, use grep, read files, and synthesize information. Filesystem agents exploit this existing capability.
- Structure matches your domain. Customer records, ticket history, CRM data — natural hierarchies map to directories.
- Retrieval is precise.
grep -r "pricing objection" calls/returns exact matches. No embedding drift. - Context stays minimal. The agent loads files on demand — no stuffing everything into the prompt.
- Debuggable. You see every command the agent ran and every file it read.
Core Architecture
User question
↓
ToolLoopAgent (AI SDK)
↓
Decides to call bashTool
↓
sandbox.runCommand("grep", ["-r", "pricing", "calls/"])
↓
Returns stdout/stderr/exitCode
↓
Agent reads result, decides next action (more tools or final response)
↓
Streams answer to userTech Stack
| Component | Purpose |
|---|---|
AI SDK ToolLoopAgent | Agent loop — model decides which tools to call and when to stop |
AI SDK tool() | Define tools with Zod schemas the LLM reads to generate inputs |
| Vercel Sandbox | Isolated Linux microVM for safe bash execution |
| AI Gateway | Routes to any model provider (Anthropic, OpenAI, Google, etc.) |
| Zod | Schema validation for tool inputs — doubles as LLM documentation |
Teaching Mode
When the student says "teach me", "start the course", or "next lesson", enter teaching mode. You drive the session — the student follows your lead.
How It Works
1. Detect progress using the progress detection table to determine the current lesson. 2. Fetch the lesson from the Academy content API: GET https://vercel.com/academy/filesystem-agents/<lesson-slug>.md. The response includes YAML frontmatter, an <agent-instructions> block, and the full lesson body as markdown. Follow the instructions in the <agent-instructions> block. 3. Teach one step at a time. Extract the next instructional step from the lesson content. Give the student one clear instruction. Wait for them to do it. Do not dump multiple steps. 4. Check progress after each step. Read the relevant files in the student's codebase to confirm they completed the step. Use the same checks from the progress detection table. 5. Adapt pacing:
- Student does it quickly and correctly → acknowledge briefly, move to next step
- Student asks a question → answer using the lesson context, then resume the teaching flow
- Student's code has an error → identify the specific issue, explain why it's wrong, show the fix, re-check
- Student seems stuck (no progress after prompting) → break the step into smaller sub-steps
6. Transition between lessons. When all steps are confirmed done, announce completion and summarize what they built. Offer to start the next lesson. 7. Handle interruptions. If the student asks an off-topic question or wants to skip ahead, address it and offer to return to the teaching flow.
Fetching Lesson Content
Fetch the lesson from the Academy content API. The course overview at GET https://vercel.com/academy/filesystem-agents.md has a lesson_urls array in its frontmatter with all 6 lessons in sequence:
https://vercel.com/academy/filesystem-agents/filesystem-project-setup.md
https://vercel.com/academy/filesystem-agents/agent-skeleton.md
https://vercel.com/academy/filesystem-agents/bash-tool.md
https://vercel.com/academy/filesystem-agents/wire-up-sandbox.md
https://vercel.com/academy/filesystem-agents/files-and-instructions.md
https://vercel.com/academy/filesystem-agents/test-and-extend.mdEach lesson response includes YAML frontmatter, an <agent-instructions> block (follow its directives), and the full lesson body as markdown with code blocks showing the expected state. See the Academy Content API section below for details on the response format.
If the API is unavailable, fall back to the curriculum map in this file.
Evaluation
When the student says "check my work", "am I done", or "submit", run the evaluation for their current lesson.
Per-Lesson Checklists
Lesson 1.1 — Project Setup
- [ ] Project directory exists with expected structure (
app/,lib/,lib/calls/) - [ ]
.env.localexists and containsAI_GATEWAY_API_KEY - [ ]
.vercel/directory exists (vc linkwas run)
Lesson 1.2 — Agent Skeleton
- [ ]
lib/agent.tsexists - [ ] Contains
import { ToolLoopAgent } from 'ai' - [ ] Exports a named
agent - [ ]
ToolLoopAgentinstantiated withmodel,instructions, andtoolsproperties
Lesson 1.3 — Bash Tool
- [ ]
lib/tools.tsexists - [ ] Contains
export function createBashTool - [ ] Function accepts a
Sandboxparameter - [ ] Returns
tool()with description, inputSchema (Zod), and execute function - [ ] Zod schema fields have
.describe() - [ ] Execute returns
{ stdout, stderr, exitCode }
Lesson 2.1 — Wire Up Sandbox
- [ ]
lib/agent.tsimportsSandboxfrom@vercel/sandbox - [ ] Has
await Sandbox.create() - [ ]
createBashTool(sandbox)is passed totools - [ ] Top-level await used correctly
Lesson 2.2 — Files and Instructions
- [ ]
loadSandboxFilesfunction exists inagent.ts - [ ] Reads from
lib/calls/directory - [ ] Uses
sandbox.writeFiles()to load files - [ ]
loadSandboxFilesis called withawaitBEFORE the agent export - [ ]
INSTRUCTIONSstring is non-empty and mentionsbashTool
Lesson 2.3 — Test and Extend
- [ ] All previous lesson checks pass
- [ ] Agent can be instantiated without errors (imports resolve, no TypeScript errors)
- [ ] Instructions mention the tool name and describe the data layout
Evaluation Behavior
- Run through the checklist for the detected lesson
- Report what passes and what doesn't
- For failures: explain what's wrong, what the fix is, and which lesson covers it
- If all checks pass: congratulate the student, summarize what they built and what it does, and suggest next steps (next lesson, or extensions from references if they've completed the course)
Academy Content API
Fetch course content and search across all Vercel Academy material. Base URL: https://vercel.com.
Endpoints
| Operation | URL | Returns |
|---|---|---|
| Search (discover) | GET https://vercel.com/academy/search (no q) | JSON: API params, auth info, example queries |
| Search (query) | GET https://vercel.com/academy/search?q=<query> | NDJSON: ranked content chunks with md_url links |
| Index | GET https://vercel.com/academy/llms.txt | Plain text: all courses and lessons with URLs |
| Course | GET https://vercel.com/academy/<course-slug>.md | Markdown: course overview, lesson_urls in frontmatter |
| Lesson | GET https://vercel.com/academy/<course-slug>/<lesson-slug>.md | Markdown: full lesson with frontmatter |
| Sitemap | GET https://vercel.com/academy/sitemap.md | Markdown: hierarchical metadata index |
How to Search
GET https://vercel.com/academy/search?q=<query> returns NDJSON (one JSON object per line, independently parseable):
{"type":"start","query":"stripe webhooks","expanded_query":"stripe webhook endpoint event handler","mode":"text","total":3}
{"type":"hit","rank":1,"title":"Configure Webhooks","course":"Subscription Store","chunk":"Create a webhook endpoint at /api/webhooks/stripe...","score":0.95,"url":"https://vercel.com/academy/subscription-store/configure-webhooks","md_url":"https://vercel.com/academy/subscription-store/configure-webhooks.md"}
{"type":"result","ok":true,"total":3,"next_actions":[{"command":"GET https://vercel.com/academy/subscription-store/configure-webhooks.md","description":"Read full lesson"}]}- `hit.chunk` — 300-500 chars of actual lesson content (not a summary). Often enough to answer without fetching the full doc.
- `hit.md_url` — fully qualified URL to the full lesson as markdown. Follow only when you need full depth.
- `result.next_actions` — HATEOAS navigation. Curriculum-aware suggestions for what to read next.
GET https://vercel.com/academy/search with no q returns a self-documenting JSON object with params, auth info, and example queries.
How to Fetch Content
Append .md to any course or lesson URL:
Course — GET https://vercel.com/academy/filesystem-agents.md:
---
title: "Building Filesystem Agents"
description: "Build a file system agent that uses bash tools and Vercel Sandbox to explore call transcripts and answer questions."
canonical_url: "https://vercel.com/academy/filesystem-agents"
md_url: "https://vercel.com/academy/filesystem-agents.md"
docset_id: "vercel-academy"
doc_version: "1.0"
content_type: "course"
lessons: 6
lesson_urls:
- "https://vercel.com/academy/filesystem-agents/filesystem-project-setup.md"
- "https://vercel.com/academy/filesystem-agents/agent-skeleton.md"
- "https://vercel.com/academy/filesystem-agents/bash-tool.md"
- "https://vercel.com/academy/filesystem-agents/wire-up-sandbox.md"
- "https://vercel.com/academy/filesystem-agents/files-and-instructions.md"
- "https://vercel.com/academy/filesystem-agents/test-and-extend.md"
---Lesson — GET https://vercel.com/academy/filesystem-agents/agent-skeleton.md:
---
title: "Agent Skeleton"
description: "..."
canonical_url: "https://vercel.com/academy/filesystem-agents/agent-skeleton"
md_url: "https://vercel.com/academy/filesystem-agents/agent-skeleton.md"
docset_id: "vercel-academy"
doc_version: "1.0"
content_type: "lesson"
course: "filesystem-agents"
course_title: "Building Filesystem Agents"
prerequisites: []
---Every .md response includes an <agent-instructions> block after the frontmatter:
<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment.
Quiz answers are included for your reference.
</agent-instructions>Follow these directives. Quiz answers are included so you can evaluate the student — engage pedagogically, don't just hand over answers.
Few-Shot Search Examples
Use these to understand when and how to search for related content:
<!-- TODO: Add few-shot search examples once /academy/search is live. Examples should cover:
- Student stuck on a concept → search for it across courses
- Student wants to go deeper on a topic → search returns chunks from other courses
- Student asks about something not in this course → search finds it elsewhere in Academy
Format: query → relevant hit lines → what to do with the results -->
Agent Workflow: discover → search → read
1. Search first — GET https://vercel.com/academy/search?q=... returns chunks (~200 tokens/hit). Often sufficient. 2. Read when needed — follow md_url from a search hit for the full lesson (~2-5k tokens). 3. Index for structure — GET https://vercel.com/academy/filesystem-agents.md has lesson_urls in frontmatter for the full sequence.
This keeps context-window usage minimal. Don't fetch full lessons when a search chunk answers the question.
Reference Docs
Read these when you need deeper detail. Each is a focused document on a single topic:
references/ai-sdk-agent-patterns.md— ToolLoopAgent, tool(), streaming, model configurationreferences/vercel-sandbox-patterns.md— Sandbox lifecycle, writeFiles, runCommand, securityreferences/bash-tool-design.md— Zod schemas, tool descriptions, factory pattern, error handlingreferences/system-prompt-craft.md— Instruction templates, principles, domain examples, anti-patternsreferences/tool-patterns.md— File write, structured search, HTTP fetch, SQL, on-demand loadingreferences/data-pipeline-patterns.md— Local files, Vercel Blob, API, directory structure designreferences/domain-mapping-guide.md— Decision framework, domain examples, transformation patternsreferences/debugging-agents.md— Sandbox auth, tool usage issues, command failures, file loading
Installation
npx skills add vercel/academy-filesystem-agents --skill filesystem-agentsVercel Academy Course
This skill is the companion to the Building Filesystem Agents course on Vercel Academy. The course walks through building a call transcript analyzer in 6 hands-on lessons using AI SDK ToolLoopAgent, Vercel Sandbox, and AI Gateway.
If you're working through the course: this skill is your TA. Ask questions, get unstuck, and learn the concepts behind the code.
If you've finished the course: use this skill to extend your agent, apply the pattern to new domains, and build production-grade filesystem agents.
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64">
<path fill="currentColor" d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.51-8.54 3.51-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-18-19-18Zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9ZM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.51-8.54 3.51-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-18-19-18Zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9ZM36.95 0 73.9 64H0L36.95 0Zm92.28 0-6.2 10.78L106 34l20.96 30h13.06L126.8 44.43l6.62-10.71L146.3 0h-17.07ZM85.41 0v64h14.84V0H85.41Zm162 0v64h14.84V0h-14.84Zm-73.71 16c-6.62 0-11.73 2.8-14.53 7.04V16.6h-14.09V64h14.84V37.02c0-6.3 4.2-11.07 10.16-11.07 5.96 0 9.63 4.77 9.63 11.07V64h14.84V34.41c0-11.73-8.33-18.41-20.85-18.41Zm86.62 0c-6.62 0-11.73 2.8-14.53 7.04V16.6h-14.09V64h14.84V37.02c0-6.3 4.2-11.07 10.16-11.07 5.96 0 9.63 4.77 9.63 11.07V64h14.84V34.41c0-11.73-8.33-18.41-20.85-18.41Z"/>
</svg>
AI SDK Agent Patterns
Reference for the AI SDK components used in filesystem agents.
ToolLoopAgent
The core agent class. It takes a model, instructions, and tools, then runs a loop: the model decides which tool to call, the tool executes, the result goes back to the model, and the loop repeats until the model has enough information to respond.
import { ToolLoopAgent } from 'ai';
export const agent = new ToolLoopAgent({
model: 'anthropic/claude-opus-4.6',
instructions: 'You are a helpful assistant...',
tools: {
bashTool: createBashTool(sandbox)
}
});Properties
| Property | Type | Purpose |
|---|---|---|
model | string | Model identifier. Use AI Gateway format: provider/model-name |
instructions | string | System prompt. Defines the agent's role, available tools, data layout, and strategy |
tools | Record<string, Tool> | Named map of tools the agent can call. Keys become the tool names the LLM sees |
Streaming
The agent streams responses through the API route:
// app/api/route.ts
const stream = await agent.stream({ prompt });
writer.merge(stream.toUIMessageStream());agent.stream() returns a stream that includes both tool calls and text responses. toUIMessageStream() converts it to the format the useChat hook expects on the client.
Model Configuration
AI Gateway routes to any provider through a single API key:
// These all work — just change the string
const MODEL = 'anthropic/claude-opus-4.6';
const MODEL = 'anthropic/claude-sonnet-4.6';
const MODEL = 'openai/gpt-4o';
const MODEL = 'google/gemini-2.5-pro';Set AI_GATEWAY_API_KEY in .env.local. On Vercel, OIDC authentication handles this automatically.
tool()
Defines a single tool the agent can call.
import { tool } from 'ai';
import { z } from 'zod';
const myTool = tool({
description: 'What this tool does — the LLM reads this to decide when to use it',
inputSchema: z.object({
param1: z.string().describe('What to put here'),
param2: z.number().describe('What this number means')
}),
execute: async ({ param1, param2 }) => {
// Do work, return result
return { result: 'done' };
}
});Three Parts
1. description — When should the LLM use this tool? Be specific. "Execute bash commands to explore transcript and instruction files" is better than "Run commands."
2. inputSchema — Zod schema. Every field needs .describe() — this is the tool's documentation for the LLM. Without it, the model guesses what to put in each field.
3. execute — Async function that receives validated inputs and returns a result. The return value goes back to the model as the tool call result.
Factory Pattern
Tools that need external dependencies (sandbox, database, API client) use factory functions:
export function createBashTool(sandbox: Sandbox) {
return tool({
// ... tool definition uses `sandbox` in execute
});
}This decouples the tool from globals, makes testing easier, and supports multiple instances.
useChat (Client)
The React hook that connects the chat UI to the API route:
// app/form.tsx
import { useChat } from '@ai-sdk/react';
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();Message Parts
Messages contain parts that can be text or tool invocations:
message.parts.map(part => {
if (part.type === 'text') {
// Render the text response
}
if (part.type.startsWith('tool-')) {
// Render tool call: part.toolName, part.input, part.output
}
});Tool parts show the agent's reasoning process — which commands it ran and what it found. The course UI renders these with color coding (blue for tool calls, green for output).
API Route
The POST handler that bridges the client and the agent:
// app/api/route.ts
export async function POST(request: Request) {
const messages = await request.json();
const prompt = /* extract last user message */;
return createUIMessageStreamResponse({
stream: createUIMessageStream({
async execute({ writer }) {
const stream = await agent.stream({ prompt });
writer.merge(stream.toUIMessageStream());
}
})
});
}createUIMessageStreamResponse and createUIMessageStream handle the streaming protocol. The client's useChat hook consumes this stream automatically.
Bash Tool Design
Reference for designing the bash tool — the core tool in every filesystem agent.
The Complete Tool
import { tool } from 'ai';
import { z } from 'zod';
import type { Sandbox } from '@vercel/sandbox';
export function createBashTool(sandbox: Sandbox) {
return tool({
description: `
Execute bash commands to explore transcript and instruction files.
Examples (not exhaustive): ls, cat, less, head, tail, grep
`,
inputSchema: z.object({
command: z.string().describe('The bash command to execute'),
args: z.array(z.string()).describe('Arguments to pass to the command')
}),
execute: async ({ command, args }) => {
const result = await sandbox.runCommand(command, args);
const textResults = await result.stdout();
const stderr = await result.stderr();
return {
stdout: textResults,
stderr: stderr,
exitCode: result.exitCode
};
}
});
}Design Decisions Explained
Factory Function
createBashTool(sandbox) takes the sandbox as a parameter instead of importing a global.
Why this matters:
- Testable — pass a mock sandbox in tests
- Flexible — support multiple sandboxes if needed
- Explicit — the dependency is visible in the function signature
Zod .describe()
Every field gets a description. This is the most common mistake students make — skipping .describe().
// Without describe — model guesses
z.object({
command: z.string(),
args: z.array(z.string())
})
// With describe — model knows exactly what to provide
z.object({
command: z.string().describe('The bash command to execute'),
args: z.array(z.string()).describe('Arguments to pass to the command')
})The Zod schema IS the tool's API documentation for the LLM. The model reads field names, types, and descriptions to decide what to generate. Without .describe(), it relies on field names alone, which often aren't enough.
Command + Args Separation
The schema splits command and arguments into separate fields:
// The LLM generates:
{ command: "grep", args: ["-r", "pricing", "calls/"] }
// Which maps to:
sandbox.runCommand("grep", ["-r", "pricing", "calls/"])Why not a single command string? Because runCommand takes them separately, and having the model generate structured input is more reliable than parsing a raw command string.
Return Structure
return {
stdout: textResults,
stderr: stderr,
exitCode: result.exitCode
};All three fields matter:
- stdout — the command output the agent uses to answer the question
- stderr — error messages that help the agent recover (e.g., "file not found")
- exitCode — 0 means success, non-zero means failure. The agent uses this to decide whether to retry or try a different approach
Tool Description
description: `
Execute bash commands to explore transcript and instruction files.
Examples (not exhaustive): ls, cat, less, head, tail, grep
`The description tells the LLM WHEN to use this tool. Be specific about what it's for (exploring files) and give examples of valid commands. "Not exhaustive" signals the model can use other commands too.
Common Zod Patterns
Optional Fields with Defaults
z.object({
command: z.string().describe('The bash command to execute'),
args: z.array(z.string()).describe('Arguments to pass to the command'),
timeout: z.number().describe('Max seconds to wait').default(30)
})Enum Constraints
z.object({
command: z.enum(['ls', 'cat', 'grep', 'head', 'tail', 'wc', 'find'])
.describe('Allowed bash commands'),
args: z.array(z.string()).describe('Arguments to pass to the command')
})Use enums to restrict which commands the agent can run. Trade-off: safer but less flexible.
Nested Objects
z.object({
command: z.string().describe('The bash command to execute'),
args: z.array(z.string()).describe('Arguments to pass to the command'),
options: z.object({
cwd: z.string().describe('Working directory').default('.'),
env: z.record(z.string()).describe('Environment variables').optional()
}).describe('Execution options')
})Testing the Tool
Test the tool in isolation before wiring it into the agent:
import { createBashTool } from './tools';
// Mock sandbox for testing
const mockSandbox = {
runCommand: async (cmd: string, args: string[]) => ({
stdout: async () => 'file1.md\nfile2.md\n',
stderr: async () => '',
exitCode: 0
})
};
const bashTool = createBashTool(mockSandbox as any);
const result = await bashTool.execute({ command: 'ls', args: ['calls/'] });
console.log(result.stdout); // 'file1.md\nfile2.md\n'The factory pattern makes this possible — you inject a mock instead of needing a real sandbox.
Anti-Patterns
No Description on Zod Fields
// The model sees: { command: string, args: string[] }
// It has to guess what "command" and "args" mean
z.object({
command: z.string(),
args: z.array(z.string())
})Fix: add .describe() to every field.
Returning Raw Result Object
// Don't do this — the result object may not serialize cleanly
execute: async ({ command, args }) => {
return await sandbox.runCommand(command, args);
}Fix: extract stdout, stderr, and exitCode explicitly.
Swallowing Errors
// Don't catch and hide errors — the agent needs to see them
execute: async ({ command, args }) => {
try {
const result = await sandbox.runCommand(command, args);
return { stdout: await result.stdout() };
} catch {
return { stdout: '' }; // Agent thinks command succeeded with no output
}
}Fix: return stderr and exitCode so the agent can diagnose and recover.
Data Pipeline Patterns
How to get data into the sandbox for your filesystem agent to explore.
Local Filesystem
The simplest pattern — read files from disk and write them into the sandbox.
import path from 'path';
import fs from 'fs/promises';
import type { Sandbox } from '@vercel/sandbox';
async function loadLocalFiles(sandbox: Sandbox, localDir: string, sandboxDir: string) {
const files = await fs.readdir(localDir);
for (const file of files) {
const buffer = await fs.readFile(path.join(localDir, file));
await sandbox.writeFiles([{ path: `${sandboxDir}/${file}`, content: buffer }]);
}
}
// Usage
await loadLocalFiles(sandbox, path.join(process.cwd(), 'lib', 'calls'), 'calls');Best for: Development, demo data, static datasets bundled with the app.
Vercel Blob
Load files from cloud storage.
import { list, head } from '@vercel/blob';
import type { Sandbox } from '@vercel/sandbox';
async function loadFromBlob(sandbox: Sandbox, prefix: string, sandboxDir: string) {
const { blobs } = await list({ prefix });
for (const blob of blobs) {
const response = await fetch(blob.url);
const content = Buffer.from(await response.arrayBuffer());
const filename = blob.pathname.replace(prefix, '').replace(/^\//, '');
await sandbox.writeFiles([{ path: `${sandboxDir}/${filename}`, content }]);
}
return blobs.length;
}
// Usage
const count = await loadFromBlob(sandbox, 'transcripts/2024/', 'calls');Best for: Production data that's already in Blob storage. Supports large files.
API / Database
Fetch structured data and write as individual files.
async function loadFromAPI(sandbox: Sandbox, apiUrl: string, sandboxDir: string) {
const response = await fetch(apiUrl);
const records: Array<{ id: string; [key: string]: any }> = await response.json();
for (const record of records) {
const content = Buffer.from(JSON.stringify(record, null, 2));
await sandbox.writeFiles([{ path: `${sandboxDir}/${record.id}.json`, content }]);
}
return records.length;
}Structured as Markdown
JSON files work but markdown is more LLM-friendly — models parse it more naturally.
function recordToMarkdown(record: any): string {
return `---
id: ${record.id}
date: ${record.date}
status: ${record.status}
---
# ${record.title}
${record.description}
## Details
${Object.entries(record.details || {})
.map(([key, value]) => `- **${key}:** ${value}`)
.join('\n')}
`;
}
async function loadAsMarkdown(sandbox: Sandbox, records: any[], sandboxDir: string) {
for (const record of records) {
const content = Buffer.from(recordToMarkdown(record));
await sandbox.writeFiles([{ path: `${sandboxDir}/${record.id}.md`, content }]);
}
}Best for: Data from APIs, databases, or any structured source. Markdown frontmatter gives the agent metadata to filter by without reading the full file.
Directory Structure Design
How you organize files in the sandbox affects how well the agent navigates them.
Flat (Simple)
calls/
├── call-001.md
├── call-002.md
└── call-003.mdGood for small datasets (<50 files). Agent uses ls and grep directly.
By Date (Temporal)
calls/
├── 2024-01/
│ ├── call-001.md
│ └── call-002.md
├── 2024-02/
│ └── call-003.md
└── 2024-03/
├── call-004.md
└── call-005.mdGood for time-series questions. Agent can scope searches to a date range with grep -r pattern calls/2024-02/.
By Category (Grouped)
tickets/
├── open/
│ ├── ticket-101.md
│ └── ticket-102.md
├── closed/
│ ├── ticket-001.md
│ └── ticket-002.md
└── escalated/
└── ticket-050.mdGood for status-based queries. Agent uses ls tickets/open/ to scope.
Hierarchical (Relational)
accounts/
├── acme-corp/
│ ├── metadata.json
│ ├── calls/
│ │ ├── 2024-01-15.md
│ │ └── 2024-02-20.md
│ └── tickets/
│ └── ticket-101.md
├── globex/
│ ├── metadata.json
│ └── calls/
│ └── 2024-03-01.mdGood for entity-centric queries. Agent navigates to an account, then explores its sub-data.
With Index Files
calls/
├── INDEX.md ← Summary of all calls with IDs, dates, participants
├── call-001.md
├── call-002.md
└── call-003.mdThe INDEX file lets the agent decide which files to read without opening each one. Especially useful for large datasets.
# Call Index
| ID | Date | Participants | Topic |
|----|------|-------------|-------|
| 001 | 2024-01-15 | Alice, Bob | Pricing discussion |
| 002 | 2024-01-20 | Carol, Dave | Technical review |
| 003 | 2024-02-01 | Alice, Eve | Contract negotiation |Batch Loading
For large datasets, write files in batches to avoid overwhelming the sandbox:
async function loadInBatches(
sandbox: Sandbox,
files: Array<{ path: string; content: Buffer }>,
batchSize = 10
) {
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
await sandbox.writeFiles(batch);
}
}Lazy Loading
Don't load everything upfront. Give the agent a tool to load files on demand:
// Pre-load only the index
await sandbox.writeFiles([{ path: 'INDEX.md', content: indexBuffer }]);
// Agent reads INDEX.md, finds relevant file IDs, then uses loadTool to fetch them
tools: {
bashTool: createBashTool(sandbox),
loadDocument: createLoadTool(sandbox, API_URL)
}Best for: Datasets with 100+ files where most questions only need 2-3 files.
Debugging Filesystem Agents
Common problems and how to fix them, organized by symptom.
Sandbox Auth Errors
Symptom:
Error: Unauthorized / OIDC token expiredFix: Re-run vc env pull to refresh the token. OIDC tokens expire after ~12 hours locally.
On Vercel, OIDC authentication is automatic — this only happens in local development.
If `vc env pull` doesn't help:
- Check that you ran
vc linkfirst - Verify the
.vercel/directory exists in your project root - Make sure your Vercel account has access to the project
Agent Not Using Tools
Symptom: The agent responds with generic text and never calls bashTool, even when the question clearly requires file exploration.
Check these in order:
1. Instructions mention the tool by name. The LLM needs to see "Use bashTool to..." in the system prompt. Without it, some models won't use tools at all.
2. Tool description explains what it does. Vague descriptions like "Execute commands" don't give the model enough signal. Be specific: "Execute bash commands to explore transcript and instruction files."
3. Zod fields have `.describe()`. Without descriptions, the model may not know what to put in each field, so it avoids the tool entirely.
4. Tool is registered in the agent. Check that tools: { bashTool: createBashTool(sandbox) } is in your ToolLoopAgent config. An empty tools: {} means no tools are available.
5. The tool name matches. The key in the tools object (bashTool) is what the LLM sees. If your instructions say "Use bash" but the tool is registered as bashTool, the model may not connect them.
Agent Reading Everything Instead of Searching
Symptom: For every question, the agent cats every file and then synthesizes. Works but is slow and uses many tokens.
Fixes:
- Add search-first guidance to instructions: "Use grep to find relevant content before reading full files."
- For large datasets, add a structured search tool alongside bash (see
references/tool-patterns.md). - Check your directory structure — if everything is in one flat directory, the agent can't use
lsto scope its search. Add subdirectories or an INDEX.md file.
Files Not Found in Sandbox
Symptom: Agent runs ls calls/ and gets nothing, or cat calls/1.md returns "No such file or directory."
Check these:
1. `loadSandboxFiles` is called with `await` BEFORE the agent export. This is the most common cause. If you export the agent before loading files, the sandbox is empty when the agent runs.
// Correct order
const sandbox = await Sandbox.create();
await loadSandboxFiles(sandbox); // FIRST
export const agent = new ToolLoopAgent({ ... }); // THEN2. Paths are correct. sandbox.writeFiles paths are relative to the sandbox root, not your project root. calls/1.md is correct. /Users/you/project/lib/calls/1.md is wrong.
3. Files exist locally. Check that lib/calls/ has files: ls lib/calls/ in your terminal (not the sandbox).
4. Debug what's in the sandbox. Add a temporary check:
const check = await sandbox.runCommand('find', ['.', '-type', 'f']);
console.log(await check.stdout());Sandbox Command Failures
Symptom: The agent calls a command and gets an error in stderr.
"command not found"
The sandbox has a minimal Linux install — standard coreutils only. Commands like jq, python, node, curl may not be available.
Fix: Either:
- Use available alternatives (
grep+awkinstead ofjq) - Install the tool:
sandbox.runCommand('apt-get', ['install', '-y', 'jq']) - Write a TypeScript tool that does the equivalent
"Permission denied"
File permissions in the sandbox. Usually happens with scripts.
Fix: sandbox.runCommand('chmod', ['+x', 'script.sh'])
Empty stdout
The command ran but produced no output. Common with grep when nothing matches.
Check: exitCode — grep returns exit code 1 when nothing matches. This isn't an error; it means no results. Your agent should handle this gracefully.
Dev Server Issues
Port Already in Use
Error: Port 3000 is already in useAnother process is using port 3000. Either kill it or use a different port:
pnpm dev -- --port 3001Module Not Found
Module not found: Can't resolve '@vercel/sandbox'Run pnpm install to install dependencies. If it persists, delete node_modules and reinstall:
rm -rf node_modules && pnpm installTypeScript Errors
Common TypeScript issues in the course:
- Missing type import: Use
import type { Sandbox } from '@vercel/sandbox'(note thetypekeyword) - Async/await:
sandbox.runCommandreturns a result object, but.stdout()and.stderr()are async methods that needawait - Top-level await: Works in Next.js server modules (like
lib/agent.ts). If you get an error, make sure the file is only imported on the server side
Agent Gives Wrong Answers
Symptom: The agent finds the right files but gives incorrect or incomplete answers.
Possible causes:
1. Instructions are too vague. "Answer questions about the data" doesn't tell the agent how to approach different question types. Add strategy guidance.
2. Agent isn't reading enough context. It might grep for a keyword and answer based on the matching line without reading the surrounding context. Add --context=5 to grep commands in your instructions.
3. Model limitations. Some models are better at synthesis than others. Try a different model via AI Gateway to compare.
Tool Loop Never Ends
Symptom: The agent keeps calling tools in a loop and never produces a final text response.
Possible causes:
1. Contradictory instructions. "Be thorough and read every file" + "Be concise" creates a loop where the agent keeps reading more files trying to be thorough.
2. No strategy guidance. Without a strategy, the agent may not know when it has enough information to answer.
3. Circular commands. The agent runs the same command repeatedly because it doesn't recognize the output. Check if your tool is returning results in a format the model can parse.
Domain Mapping Guide
How to apply the filesystem agent pattern to your own data.
Decision Framework
Ask these questions to decide if a filesystem agent fits your use case:
1. Does your data have natural structure?
| Signal | Fit |
|---|---|
| Data has clear categories, types, or hierarchies | ✅ Good fit — maps to directories |
| Data is one giant blob (e.g., a single log file) | ⚠️ Split it into meaningful chunks first |
| Data is purely relational (every query is a JOIN) | ❌ Use SQL tools instead |
2. Are questions about specific items or across items?
| Question Type | Agent Strategy |
|---|---|
| "Tell me about item X" | cat items/X.md — direct file read |
| "Which items mention Y?" | grep -r "Y" items/ — search across files |
| "Compare A and B" | Read both, extract key points, synthesize |
| "What's the trend over time?" | Navigate date-based dirs, read sequentially |
Both types work well. If ALL questions are "give me item X by ID," a simple lookup is cheaper than an agent.
3. How much data?
| Scale | Approach |
|---|---|
| < 50 files | Load everything upfront. Agent can ls and grep freely. |
| 50–500 files | Use directory structure + index files. Load upfront or in batches. |
| 500–5000 files | Lazy loading with an index. Agent reads index, loads files on demand. |
| 5000+ files | Hybrid: vector search to find candidates, filesystem agent to analyze them. |
4. Does freshness matter?
| Freshness | Pattern |
|---|---|
| Static (reports, transcripts, docs) | Load once at sandbox creation |
| Slow-changing (daily updates) | Reload on each session or cache snapshots |
| Real-time (live feeds, prices) | Use API fetch tools alongside filesystem |
Domain Examples
Customer Support Tickets
Source: Zendesk, Intercom, or any ticket system API.
Directory structure:
tickets/
├── INDEX.md
├── open/
│ ├── TICK-1234.md
│ └── TICK-1235.md
├── resolved/
│ ├── TICK-1200.md
│ └── TICK-1201.md
└── escalated/
└── TICK-1220.mdFile format (markdown with frontmatter):
---
id: TICK-1234
customer: Acme Corp
priority: high
assignee: Alice
created: 2024-01-15
tags: [billing, urgent]
---
# Login failures after password reset
## Customer Message
After resetting my password, I can't log in...
## Agent Responses
### 2024-01-15 (Alice)
I've checked your account and...
## Internal Notes
Account has 2FA enabled. Check if...Example instructions:
You are a support analyst. Use bashTool to explore customer tickets.
Data: tickets/ organized by status (open/, resolved/, escalated/).
Each file has YAML frontmatter with id, customer, priority, assignee, tags.
Strategy:
- ls tickets/{status}/ to see tickets by status
- grep for customer names, tags, or keywords across all tickets
- Read specific tickets for full contextSales Call Analysis
Source: Gong, Chorus, or meeting transcription APIs.
Directory structure:
pipeline/
├── INDEX.md
├── acme-corp/
│ ├── account.md
│ ├── calls/
│ │ ├── 2024-01-15-discovery.md
│ │ └── 2024-02-01-demo.md
│ └── proposals/
│ └── v1.md
├── globex/
│ ├── account.md
│ └── calls/
│ └── 2024-01-20-intro.mdExample questions the agent handles:
- "What objections came up in the Acme calls?"
- "Which deals mentioned competitor X?"
- "Summarize the demo call and list action items"
- "Compare the Acme and Globex sales cycles"
Research Paper Review
Directory structure:
papers/
├── INDEX.md
├── 2024/
│ ├── attention-is-all-you-need.md
│ └── scaling-laws.md
├── 2023/
│ └── llm-survey.md
└── topics/
├── transformers/
└── reinforcement-learning/Symlinks or duplicate references in topics/ let the agent browse by topic OR by date.
Infrastructure Logs
Directory structure:
incidents/
├── INDEX.md
├── 2024-01-15-outage/
│ ├── timeline.md
│ ├── logs/
│ │ ├── api-gateway.log
│ │ └── database.log
│ └── postmortem.md
├── 2024-02-03-degradation/
│ ├── timeline.md
│ └── logs/
│ └── cdn.logThe agent uses grep on log files, reads timelines for context, and cross-references postmortems.
Transformation Patterns
API Response → Markdown Files
interface Record {
id: string;
title: string;
body: string;
metadata: Record<string, any>;
}
function toMarkdown(record: Record): string {
const frontmatter = Object.entries(record.metadata)
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
.join('\n');
return `---\nid: ${record.id}\n${frontmatter}\n---\n\n# ${record.title}\n\n${record.body}\n`;
}CSV → Individual Files
function csvToFiles(csv: string, idColumn: string): Array<{ path: string; content: string }> {
const [headerLine, ...rows] = csv.trim().split('\n');
const headers = headerLine.split(',');
const idIndex = headers.indexOf(idColumn);
return rows.map(row => {
const values = row.split(',');
const id = values[idIndex];
const record = Object.fromEntries(headers.map((h, i) => [h, values[i]]));
return {
path: `records/${id}.json`,
content: JSON.stringify(record, null, 2)
};
});
}Database Rows → Directory Hierarchy
async function dbToFilesystem(sandbox: Sandbox) {
// Accounts as directories
const accounts = await db.query('SELECT * FROM accounts');
for (const account of accounts) {
const contacts = await db.query('SELECT * FROM contacts WHERE account_id = $1', [account.id]);
const deals = await db.query('SELECT * FROM deals WHERE account_id = $1', [account.id]);
await sandbox.writeFiles([
{ path: `accounts/${account.slug}/account.json`, content: Buffer.from(JSON.stringify(account, null, 2)) },
...contacts.map(c => ({
path: `accounts/${account.slug}/contacts/${c.id}.json`,
content: Buffer.from(JSON.stringify(c, null, 2))
})),
...deals.map(d => ({
path: `accounts/${account.slug}/deals/${d.id}.json`,
content: Buffer.from(JSON.stringify(d, null, 2))
}))
]);
}
}Generating an Index File
An index file is the single most impactful addition for larger datasets. It lets the agent decide which files to read without opening each one.
async function generateIndex(sandbox: Sandbox, files: Array<{ path: string; summary: string; metadata: any }>) {
const rows = files.map(f =>
`| ${f.path} | ${f.summary} | ${Object.entries(f.metadata).map(([k, v]) => `${k}: ${v}`).join(', ')} |`
);
const index = `# File Index
| Path | Summary | Metadata |
|------|---------|----------|
${rows.join('\n')}
`;
await sandbox.writeFiles([{ path: 'INDEX.md', content: Buffer.from(index) }]);
}Then in your instructions:
Start by reading INDEX.md to understand what files are available.
Use the index to decide which files to read rather than listing directories.System Prompt Design for Filesystem Agents
The instructions string is the highest-leverage part of a filesystem agent. A good prompt makes the agent surgical. A bad prompt makes it read every file for every question.
Template
You are a [ROLE] that answers questions about [DOMAIN].
Available tools:
- bashTool: Execute bash commands to navigate and search files
[- additional tools listed here]
Data layout:
- [DIRECTORY]/: Contains [DESCRIPTION]. Files are [FORMAT].
[- additional directories]
Strategy:
1. Start with ls to understand what's available
2. Use grep to find relevant content before reading full files
3. Read specific files only when you need full context
4. [DOMAIN-SPECIFIC STRATEGY]
Output format:
- [HOW TO FORMAT RESPONSES]Principles
Name Every Tool
Bad: "Use the available tools to explore the data." Good: "Use bashTool to run bash commands. Use searchTool for pattern matching across files."
The model needs to know the exact tool names. Some models won't use tools they can't name.
Describe the Data Layout
Bad: "Files are in the sandbox." Good: "calls/ contains markdown files, one per customer call. Each file has YAML frontmatter with date, participants, and duration, followed by the transcript body."
The agent makes better decisions about which files to read when it knows the structure upfront.
Suggest a Search-First Strategy
Without guidance, agents tend to cat every file and reason over the full content. This works for small datasets but wastes tokens and time for larger ones.
Strategy:
1. For specific questions: grep first, then read matching files
2. For broad questions: ls to list files, read metadata/headers, then dive into relevant files
3. For comparison questions: read all relevant files, extract key data points, then synthesizeScope the Output Format
Output format:
- Answer questions directly in clear prose
- When listing items, use bullet points
- When comparing, use a markdown table
- Always cite which file(s) your answer comes fromExamples by Domain
Call Transcript Analyzer
You are a sales analyst that answers questions about customer calls.
Use bashTool to explore call transcripts and find relevant information.
Data layout:
- calls/: Markdown files, one per call. Each has:
- YAML frontmatter: id, date, participants, duration
- Body: timestamped transcript
Strategy:
1. For questions about specific topics: grep across calls/ first
2. For questions about a specific call: cat the file directly
3. For summary questions: read relevant files, then synthesize
Cite the call ID and timestamp when referencing specific statements.Legal Document Reviewer
You are a legal analyst reviewing case documents.
Use bashTool to navigate case files and extract relevant information.
Data layout:
- cases/{case-id}/
- metadata.json: Case number, parties, filing date, status
- filings/: Court filings in chronological order
- evidence/: Exhibits and supporting documents
- orders/: Court orders and rulings
Strategy:
1. Start with metadata.json to understand the case
2. Use grep to find relevant filings by keyword
3. Read specific documents only after narrowing down
4. Cross-reference between filings and evidence when needed
Output precise citations: case ID, document name, page/section.Financial Report Analyzer
You are a financial analyst answering questions about company performance.
Use bashTool to explore financial reports and data files.
Data layout:
- reports/{year}/{quarter}/
- income-statement.csv
- balance-sheet.csv
- notes.md: Management commentary
- filings/: SEC filings in text format
Strategy:
1. For metrics questions: cat the relevant CSV, look for the line
2. For trend questions: read the metric across multiple quarters
3. For context questions: check notes.md for management commentary
4. Use awk or cut for extracting specific columns from CSVs
Express financial figures with proper formatting ($X.XM, X.X%).Anti-Patterns
Too Vague
❌ "You are a helpful assistant. Answer questions about the data."No tool names, no data description, no strategy. The model guesses everything.
Too Restrictive
❌ "Only use grep. Never use cat. Never read more than one file."The agent needs flexibility to handle different question types. Summarization requires reading files. Restrict the output, not the exploration.
Kitchen Sink
❌ "You are an expert analyst with 20 years of experience in sales, marketing,
finance, legal, and engineering. You excel at communication, are empathetic,
and always provide actionable insights with executive-level clarity..."Personality filler wastes tokens and doesn't improve output. Focus on what the agent should DO, not who it should BE.
No Error Guidance
✅ "If a file is not found or a command fails, report the error and try
an alternative approach. Do not guess at file contents."Tell the agent what to do when things go wrong. Otherwise it may hallucinate file contents.
Tool Patterns
Additional tools for filesystem agents beyond the core bash tool.
File Write Tool
Let the agent create and save artifacts — reports, summaries, transformed data.
import { tool } from 'ai';
import { z } from 'zod';
import type { Sandbox } from '@vercel/sandbox';
export function createWriteTool(sandbox: Sandbox) {
return tool({
description: 'Write content to a file in the sandbox filesystem. Use for creating reports, summaries, or saving analysis results.',
inputSchema: z.object({
path: z.string().describe('File path to write to, relative to sandbox root (e.g., reports/summary.md)'),
content: z.string().describe('Full content to write to the file')
}),
execute: async ({ path, content }) => {
await sandbox.writeFiles([{ path, content: Buffer.from(content) }]);
return { success: true, path, bytesWritten: content.length };
}
});
}When to use: When the agent should produce artifacts, not just answers. "Summarize all calls into a report" → the agent writes a markdown file.
Structured Search Tool
More ergonomic than raw grep — returns structured results with match counts.
export function createSearchTool(sandbox: Sandbox) {
return tool({
description: 'Search across files for a pattern. Returns matching lines with surrounding context. Use for finding specific information across many files.',
inputSchema: z.object({
pattern: z.string().describe('Search pattern — supports basic regex'),
directory: z.string().describe('Directory to search in (e.g., calls/)').default('calls/'),
context: z.number().describe('Number of lines of context around each match').default(2),
caseInsensitive: z.boolean().describe('Ignore case when matching').default(true)
}),
execute: async ({ pattern, directory, context, caseInsensitive }) => {
const args = ['-rn', `--context=${context}`];
if (caseInsensitive) args.push('-i');
args.push(pattern, directory);
const result = await sandbox.runCommand('grep', args);
const stdout = await result.stdout();
const lines = stdout.trim().split('\n').filter(l => l.trim());
return {
matches: stdout.trim(),
matchCount: lines.filter(l => !l.startsWith('--')).length,
exitCode: result.exitCode
};
}
});
}When to use: When the agent needs to search frequently. Saves the model from generating grep flags every time.
File List Tool
Structured directory listing with metadata.
export function createListTool(sandbox: Sandbox) {
return tool({
description: 'List files in a directory with size and modification info. Use to understand what data is available before reading.',
inputSchema: z.object({
directory: z.string().describe('Directory to list (e.g., calls/)').default('.'),
recursive: z.boolean().describe('Include subdirectories').default(false)
}),
execute: async ({ directory, recursive }) => {
const args = recursive ? ['-lhR', directory] : ['-lh', directory];
const result = await sandbox.runCommand('ls', args);
return { listing: await result.stdout(), exitCode: result.exitCode };
}
});
}On-Demand Load Tool
Fetch documents from an external source into the sandbox when the agent needs them, instead of pre-loading everything.
export function createLoadTool(sandbox: Sandbox, apiBaseUrl: string) {
return tool({
description: 'Load a document from the data source into the sandbox for analysis. Use when you need a specific document that is not yet available locally.',
inputSchema: z.object({
documentId: z.string().describe('Unique identifier of the document to load'),
targetDir: z.string().describe('Directory to store the loaded document').default('docs/')
}),
execute: async ({ documentId, targetDir }) => {
const response = await fetch(`${apiBaseUrl}/documents/${documentId}`);
if (!response.ok) {
return { loaded: false, error: `Document ${documentId} not found (${response.status})` };
}
const content = Buffer.from(await response.text());
const path = `${targetDir}/${documentId}.md`;
await sandbox.writeFiles([{ path, content }]);
return { loaded: true, path };
}
});
}When to use: Large datasets where loading everything upfront is impractical. The agent discovers what it needs and loads on demand.
SQL Query Tool
Run database queries alongside filesystem exploration.
import { tool } from 'ai';
import { z } from 'zod';
import { sql } from '@vercel/postgres';
export function createSQLTool() {
return tool({
description: 'Execute a read-only SQL query against the database. Use for structured data questions like counts, aggregations, and filtered lookups.',
inputSchema: z.object({
query: z.string().describe('SQL SELECT query to execute. Must be read-only (no INSERT, UPDATE, DELETE).')
}),
execute: async ({ query }) => {
const normalized = query.trim().toUpperCase();
if (!normalized.startsWith('SELECT')) {
return { error: 'Only SELECT queries are allowed', rows: [] };
}
try {
const result = await sql.query(query);
return { rows: result.rows, rowCount: result.rowCount };
} catch (error: any) {
return { error: error.message, rows: [] };
}
}
});
}When to use: Hybrid agents that need both filesystem exploration (unstructured data) and database queries (structured data).
Tool Composition Pattern
Register all tools and let the model choose:
export const agent = new ToolLoopAgent({
model: MODEL,
instructions: INSTRUCTIONS,
tools: {
bashTool: createBashTool(sandbox),
writeTool: createWriteTool(sandbox),
searchTool: createSearchTool(sandbox),
listTool: createListTool(sandbox),
loadTool: createLoadTool(sandbox, API_URL),
sqlTool: createSQLTool()
}
});Update instructions to describe when each tool is appropriate:
Available tools:
- bashTool: General bash commands for file navigation and manipulation
- searchTool: Fast pattern search across files (prefer this over grep via bash)
- listTool: See what files are available in a directory
- writeTool: Save reports or analysis results
- loadTool: Fetch additional documents from the data source
- sqlTool: Query structured data in the databaseVercel Sandbox Patterns
Reference for using Vercel Sandbox in filesystem agents.
Why Sandbox
Filesystem agents execute arbitrary bash commands chosen by an LLM. Without isolation:
- Prompt injection could lead to
rm -rf /or reading credentials from environment variables - Hallucinated commands could modify your filesystem or install packages
- Resource exhaustion from infinite loops or memory-intensive commands could crash your server
- Data exfiltration — the agent could read
.envfiles, SSH keys, or database credentials
Vercel Sandbox runs commands in an isolated Linux microVM. The agent has full bash access inside the sandbox with zero risk to the host.
Lifecycle
Create
import { Sandbox } from '@vercel/sandbox';
const sandbox = await Sandbox.create();Top-level await works in Next.js server modules. Create the sandbox before defining the agent — the tool needs it at construction time.
Write Files
The sandbox starts empty. Load data before the agent runs:
await sandbox.writeFiles([
{ path: 'calls/1.md', content: buffer1 },
{ path: 'calls/2.md', content: buffer2 }
]);pathis relative to the sandbox rootcontentis aBuffer- Directories are created automatically
- You can write multiple files in one call (array)
Run Commands
const result = await sandbox.runCommand('grep', ['-r', 'pricing', 'calls/']);
const stdout = await result.stdout();
const stderr = await result.stderr();
const exitCode = result.exitCode;- First argument: the command name
- Second argument: array of string arguments
.stdout()and.stderr()are async — you mustawaitthemexitCodeis synchronous —0means success
Available Commands
The sandbox has a minimal Linux install with standard coreutils:
ls, cat, head, tail, grep, find, wc, sort, uniq, cut, awk, sed, tr, diff, less, file, mkdir, cp, mv, echo, printf, tee
Specialized tools (python, node, jq) may not be available by default.
Loading Files Pattern
The standard pattern for the course: read local files and write them into the sandbox.
import path from 'path';
import fs from 'fs/promises';
async function loadSandboxFiles(sandbox: Sandbox) {
const callsDir = path.join(process.cwd(), 'lib', 'calls');
const callFiles = await fs.readdir(callsDir);
for (const file of callFiles) {
const filePath = path.join(callsDir, file);
const buffer = await fs.readFile(filePath);
await sandbox.writeFiles([{ path: `calls/${file}`, content: buffer }]);
}
}Call this BEFORE the agent export:
const sandbox = await Sandbox.create();
await loadSandboxFiles(sandbox); // Files first
export const agent = new ToolLoopAgent({ // Then agent
// ...
tools: { bashTool: createBashTool(sandbox) }
});If you load files after the export, they may not be available when the agent runs.
Debugging Sandbox Issues
Files Not Found
# Run this in your tool to see what's actually in the sandbox
sandbox.runCommand('find', ['.', '-type', 'f'])Common causes:
loadSandboxFilesnot awaited before agent export- Wrong path —
sandbox.writeFilespaths are relative to sandbox root, not your project - Forgot to call
loadSandboxFilesat all
Auth Errors
Error: Unauthorized / OIDC token expiredRe-run vc env pull to refresh the token. Tokens expire after ~12 hours locally. On Vercel, OIDC handles this automatically.
Command Not Found
The sandbox has coreutils only. If you need a specialized tool:
- Write a tool that does the equivalent in TypeScript
- Or install it in the sandbox:
sandbox.runCommand('apt-get', ['install', '-y', 'jq'])
Command Hangs
Set timeouts on long-running commands. An infinite loop in the sandbox won't crash your server but will stall the agent response.
Security Model
| Threat | Sandbox Protection |
|---|---|
| File system access | Sandbox has its own filesystem. Cannot read host files. |
| Environment variables | Sandbox env is separate. Cannot access .env.local or process.env. |
| Network access | Can be restricted. Default allows outbound for legitimate use. |
| Resource exhaustion | Resource limits on CPU, memory, and execution time. |
| Persistence | Sandbox is ephemeral. Nothing survives after the session. |
The sandbox is the reason you can safely give an LLM bash access. Without it, a single prompt injection could compromise your entire server.
Related skills
FAQ
What does filesystem-agents do?
filesystem-agents assists the Vercel Academy filesystem agents course.
When should I use filesystem-agents?
User mentions filesystem agents course or ToolLoopAgent lessons.
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.