Customization

Bend Laraclaw to your needs without writing a tool — system prompts, rate limiting, logging, custom commands, and event hooks.

Introduction

This page is your toolbox for bending Laraclaw without writing tools. We'll cover picking a model, controlling how often the agent can be pinged, logging every prompt for an audit trail, allowlisting disks, wiring deterministic shortcuts, and listening to agent events.

If you're after a custom tool (a new capability for the agent to call), head over to Adding Custom Tools.

Choosing an AI Provider

Laraclaw is provider-agnostic. We delegate model selection to laravel/ai, which means whichever driver you have credentials for, you can use:

AI_DEFAULT=anthropic

The setup wizard handles this for you — php artisan laraclaw:setup-agent asks which provider you want and prompts for the API key. Pretty simple, right?

Prompt Caching (Anthropic and OpenAI)

System prompts in Laraclaw are not small: the default prompt, the persona overlay, the tool definitions, and the current time all get sent on every turn. That adds up fast.

The good news is the ChatBotAgent opts into native prompt caching when the provider supports it:

  • Anthropic — sends cache_control: { type: 'ephemeral' } on the system prompt. Repeat calls within the cache TTL pay roughly 10% of the tokens for the cached portion.
  • OpenAI — sends prompt_cache_key: 'laraclaw-chatbot' so OpenAI routes repeat calls to the same cache shard.

There's nothing to configure. If you switch to a provider that doesn't support caching, it falls back to a normal call path — no error, just full-price tokens.

!TIP If you've added a custom agent and want the same caching behavior, implement Laravel\Ai\Contracts\HasProviderOptions and return the right options per Lab enum. Look at Laraclaw\Agents\ChatBotAgent::providerOptions() for a working example.

Choosing a Model

Tool-calling is the dimension that matters most. Every connector relies on the model deciding when to call a tool, and weaker models will either skip tools that should fire, hallucinate parameters, or get stuck in loops.

As a baseline, the most reliable picks at the time of writing are:

  • Anthropic Claude Sonnet (latest) — strongest tool-calling, plus prompt caching cuts repeat-call cost.
  • OpenAI GPT-4-class models with function calling enabled.
  • Local models via Ollama or similar — fine for the terminal connector and quick experiments, but expect more failed tool calls.

The smaller and cheaper the model, the more often you'll see it ignore the system prompt or fumble a tool's JSON schema. If a connector feels flaky, switch to a larger model before blaming the package.

Cost

Laraclaw doesn't bill anything itself — your AI cost is whatever your laravel/ai provider charges, multiplied by token volume. Two things drive that volume:

  • System prompt size. The base prompt + persona overlay + tool definitions are sent on every turn. With prompt caching enabled (Anthropic and OpenAI), the cached portion is nearly free on repeat calls; other providers pay full price every time.
  • Memory retrieval. When the agent calls MemoryManager, retrieved chunks are appended to the context. The default LARACLAW_MEMORY_MAX_RESULTS=5 is conservative; raising it raises tokens per call linearly.

There's no usage telemetry built in. If you need a number, enable LARACLAW_LOG_AGENT_REQUESTS and read token counts off your provider's dashboard.

The Agent Folder

Everything that shapes how the agent thinks lives in one folder at your project root, written in Markdown rather than code:

laraclaw/
  instructions.md        # base system prompt, always on
  personas/
    default.md           # voice and tone, applied automatically
    ops.md
  skills/
    greeting/
      SKILL.md           # loaded only when relevant
    release-notes/
      SKILL.md
      reference.md       # companion files ride along

vendor:publish --tag=laraclaw writes the starting point. Every file is optional: delete the lot and the agent falls back to the prompt baked into the package.

Because it is all plain text at a known path, a change to your agent's behavior shows up as a reviewable diff instead of a config value nobody remembers setting.

Customizing the System Prompt

The base system prompt lives at laraclaw/instructions.md. Edit it to change the agent's default behavior across all conversations.

Laraclaw appends the current date, timezone, and the sender's name for you, so there is no need to mention them in the file. To keep the prompt somewhere else, point LARACLAW_INSTRUCTIONS_PATH at it.

!TIP For per-conversation tweaks, use personas instead. They get appended to the base prompt without overwriting it — much more flexible than editing the default.

Rate Limiting

You probably don't want a runaway script (or a chatty group) flooding your AI provider with hundreds of calls. Each connector route enforces a rate limit so no single conversation can do that:

LARACLAW_WEBHOOK_RATE_LIMIT=20

The value is the number of inbound messages allowed per minute. The default is 20. Set it to 0 to disable rate limiting entirely.

Limits are scoped per connector and keyed differently for each:

ConnectorLimiter nameKey
Slacklaraclaw-slackslack:{channelId}:{userId} — one bucket per Slack user per channel
Telegramlaraclaw-telegramtelegram:{chatId} — one bucket per chat (DM or group)
APIlaraclaw-apiapi:{tokenAccountId} — one bucket per authenticated token
Email(none)IMAP polling, no HTTP route to throttle
Terminal(none)local CLI, no HTTP route to throttle

The backend is Laravel's standard Illuminate\Support\Facades\RateLimiter, which uses your default cache store. If that store is Redis or another shared backend, the limit is enforced consistently across multiple web nodes. If it's the file or array driver, each node has its own bucket.

Email and the terminal have no rate limit because there is no HTTP route for the throttle middleware to attach to.

Logging Agent Requests

Want to see exactly what the agent is being asked and what it's saying back? Enable request logging:

LARACLAW_LOG_AGENT_REQUESTS=true

The LogAgentRequest listener subscribes to AgentPrompted and writes a structured log entry with the prompt, response, and conversation context to the default Laravel log channel.

!NOTE Enable this when you're using Tinker. It's the closest thing to an audit trail of what the agent has been asked to do.

Allowed Disks

The File Manager and Image Manager tools can only see disks listed in:

LARACLAW_ALLOWED_DISKS=local,public,s3

Disks not in the list are invisible to the agent. The default is local. Set this carefully — anything in the list is fully readable and writable by any tool that takes a disk parameter.

Custom Commands

Sometimes you don't want the model involved at all. You want a fast, predictable, free shortcut that just runs and is done. That's what commands are for.

Laraclaw runs every inbound message through a CommandRegistry before passing it to the agent. If a command's trigger matches the message text exactly, the command handles the message and the agent never sees it. The built-in !new command resets the current conversation — that's the canonical example.

Commands are great for:

  • Switching settings ("!verbose")
  • Fetching cached data
  • Dumping debug info
  • Anything you don't want a model second-guessing

Let's see how to write one.

The Command Interface

Two methods. trigger() returns the exact text that activates the command. handle() does the work and returns either null to halt processing entirely, or a string to continue processing that string through the agent loop.

namespace Laraclaw\Commands;

use Laraclaw\DTOs\IncomingMessage;
use Laraclaw\Models\Thread;

interface Command
{
    public function trigger(): string;

    public function handle(IncomingMessage $message, Thread $thread): ?string;
}

Triggers are matched against the trimmed, lowercased message text. !new matches !new, !New, !new — but not !new yesterday. Commands are exact-match only; if you need parameters, use a tool or a skill instead.

A Worked Example

Here's the built-in !new command. It clears the laravel/ai conversation ID off the thread, posts a confirmation back through whichever connector the message came in on, and returns null to stop the message from reaching the agent:

namespace Laraclaw\Commands;

use Laraclaw\DTOs\IncomingMessage;
use Laraclaw\Models\Thread;

class NewConversation implements Command
{
    public function trigger(): string
    {
        return '!new';
    }

    public function handle(IncomingMessage $message, Thread $thread): ?string
    {
        $thread->update(['conversation_id' => null]);
        $thread->connector()->reply($thread, '✅ Conversation reset.');

        return null;
    }
}

Registering a Command

From a service provider's boot() method:

use Laraclaw\Commands\CommandRegistry;

public function boot(): void
{
    $this->app->make(CommandRegistry::class)->register(new MyCustomCommand());
}

The registry is a singleton, indexed by lowercased trigger. Registering two commands with the same trigger silently replaces the first.

Hooking Into Agent Events

Laraclaw uses the AgentPrompted event from laravel/ai for its memory and logging listeners. You can subscribe your own listener and run code on every agent turn:

use Illuminate\Support\Facades\Event;
use Laravel\Ai\Events\AgentPrompted;

public function boot(): void
{
    Event::listen(AgentPrompted::class, function (AgentPrompted $event) {
        // $event->prompt, $event->response, $event->conversationId
    });
}

Use it for analytics, custom audit logging, alerting, or piping interesting prompts into a tool of your choice.

What's Next

  • Adding Custom Tools — give the agent a brand-new capability.
  • Personas — change how the agent talks, per-thread.
  • Skills — give the agent reusable procedures.
  • Reference — every LARACLAW_* variable, in one table.

Until next time!

Copyright © 2026