Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.castari.com/llms.txt

Use this file to discover all available pages before exploring further.

Best Practices

Guidelines for building reliable, secure, and efficient agents.

Agent Design

Keep Tools Focused

Each tool should do one thing well:
// Good: Specific, focused tools
{ name: "read_file", description: "Read file contents" }
{ name: "write_file", description: "Write content to file" }
{ name: "list_files", description: "List files in directory" }

// Bad: One tool that does everything
{ name: "file_operations", description: "Read, write, delete, list files" }

Write Clear Tool Descriptions

Claude uses descriptions to decide when to use tools:
// Good: Clear, specific description
{
  name: "search_users",
  description: "Search for users by email address. Returns matching user profiles with id, name, and email."
}

// Bad: Vague description
{
  name: "search_users",
  description: "Search users"
}

Use CLAUDE.md Effectively

Structure your CLAUDE.md for clarity:
# Agent Name

One-line description of what this agent does.

## Capabilities
- What the agent CAN do
- Available tools and their purposes

## Limitations
- What the agent should NOT do
- Boundaries and restrictions

## Guidelines
- How to approach tasks
- Tone and communication style
- When to ask for clarification

## Examples
- Example inputs and expected behavior

Error Handling

Handle Tool Failures Gracefully

try {
  const result = await executeTool(toolUse.name, toolUse.input);
  toolResults.push({
    type: "tool_result",
    tool_use_id: toolUse.id,
    content: result
  });
} catch (error) {
  // Return error as tool result so Claude can adapt
  toolResults.push({
    type: "tool_result",
    tool_use_id: toolUse.id,
    content: `Error: ${error.message}`,
    is_error: true
  });
}

Validate Inputs

case "read_file":
  // Validate path exists
  if (!input.path) {
    throw new Error("path is required");
  }

  // Validate path is safe
  if (input.path.includes("..")) {
    throw new Error("path traversal not allowed");
  }

  // Check file exists
  if (!fs.existsSync(input.path)) {
    throw new Error(`file not found: ${input.path}`);
  }

  return fs.readFileSync(input.path, "utf-8");

Set Timeouts

const timeout = (ms: number) => new Promise((_, reject) =>
  setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);

// Wrap long operations
const result = await Promise.race([
  executeSlowOperation(),
  timeout(30000)
]);

Security

Never Trust User Input

// Bad: SQL injection risk
case "query_database":
  return db.query(input.query);

// Good: Parameterized queries only
case "query_database":
  return db.query(input.query, input.params);

Restrict File Access

const ALLOWED_PATHS = ['/workspace', '/tmp'];

function isPathAllowed(filePath: string): boolean {
  const resolved = path.resolve(filePath);
  return ALLOWED_PATHS.some(allowed =>
    resolved.startsWith(allowed)
  );
}

case "read_file":
  if (!isPathAllowed(input.path)) {
    throw new Error("Access denied");
  }
  return fs.readFileSync(input.path, "utf-8");

Sanitize Command Execution

// Bad: Command injection risk
case "bash":
  return execSync(input.command);

// Better: Whitelist safe commands
const ALLOWED_COMMANDS = ['ls', 'cat', 'grep', 'find'];

case "bash":
  const cmd = input.command.split(' ')[0];
  if (!ALLOWED_COMMANDS.includes(cmd)) {
    throw new Error(`Command not allowed: ${cmd}`);
  }
  return execSync(input.command);

Use Secrets for Credentials

// Bad: Hardcoded credentials
const apiKey = "sk-abc123";

// Good: Use environment variables
const apiKey = process.env.API_KEY;
if (!apiKey) {
  throw new Error("API_KEY not configured");
}

Performance

Minimize API Calls

// Bad: Multiple calls for same data
const user = await getUser(id);
const orders = await getOrders(id);
const preferences = await getPreferences(id);

// Better: Batch or cache
const userData = await getUserWithDetails(id);
// or use caching

Stream Large Outputs

For long responses, consider streaming:
// Instead of buffering everything
let output = "";
for (const chunk of results) {
  output += chunk;
}
console.log(output);

// Stream incrementally
for (const chunk of results) {
  process.stdout.write(chunk);
}

Clean Up Resources

// Close connections when done
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

process.on('exit', () => {
  pool.end();
});

Testing

Test Locally First

# Test with various inputs
echo "Simple prompt" | npm run dev
echo "What files are here?" | npm run dev
echo "Create a file called test.txt" | npm run dev

Test Edge Cases

  • Empty prompts
  • Very long prompts
  • Prompts that might cause infinite loops
  • Prompts with special characters

Test Tool Failures

echo "Read /nonexistent/file.txt" | npm run dev
# Should handle gracefully, not crash

Monitoring

Log Important Events

console.error(`[${new Date().toISOString()}] Agent started`);
console.error(`[${new Date().toISOString()}] Tool: ${name}`);
console.error(`[${new Date().toISOString()}] Tokens: ${inputTokens}/${outputTokens}`);

Track Costs

Monitor your usage:
cast usage
cast usage --daily

Deployment

Use Git for Versioning

Keep your agent in version control:
git init
git add .
git commit -m "Initial agent"

Document Your Agent

Include a README.md:
# My Agent

Description of what this agent does.

## Setup

1. `npm install`
2. Configure secrets: `cast secrets set my-agent API_KEY xxx`
3. Deploy: `cast deploy`

## Usage

cast invoke my-agent "Your prompt here"

## Development

npm run dev

See Also

Debugging

Troubleshooting tips

Secrets

Managing credentials