Laravel Packages

Laravel Tackle: Run an AI Coding Agent in Your Laravel App

Published
Laravel Tackle: Run an AI Coding Agent in Your Laravel App image

Laravel Tackle, written by Jordan Dalton, is an AI coding agent that runs as Artisan commands inside your application. Because it boots with the framework, its tools are the ones you would reach for yourself: it can list your routes, read a Telescope exception, run a SELECT against your database, and call Pint once it has finished editing.

Running inside the app also changes where the guardrails live. Path restrictions, the Artisan allowlist, and the per-session spend limit are PHP code in the package, not instructions in a prompt that a model can ignore. The package is built on laravel/ai and defaults to Claude, though AI_CODE_PROVIDER will point it at OpenAI, Gemini, Groq, or a local model through Ollama.

The package ships several agents:

  • ai:code is the interactive session: a REPL with plan mode, slash commands, image attachments, and history that survives between runs
  • ai:run is the same agent with no terminal attached: one task, an exit code, and --output=json when a pipeline needs to read the result
  • ai:fix starts a focused fix session from a pasted exception, a Sentry issue (--sentry=ID), or a GitHub issue (--issue=N)
  • ai:review reads a diff or a pull request and posts findings as inline review comments with severity levels
  • ai:upgrade takes a Composer package across a major version, working from the package's own upgrade guide in vendor/
  • The self-healer watches for failed queue jobs and scheduled tasks, patches the code in an isolated worktree, and opens a pull request
  • Tackle Remote is a companion package that serves the same harness as a mobile browser UI, approval prompts included

Installation

Tackle requires PHP 8.3 and Laravel 12 or 13. Install it, then publish both configs:

composer require jordandalton/laravel-tackle
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan vendor:publish --tag="tackle-config"

The published config/ai.php already carries the Anthropic provider block, so an API key in .env is all that is left:

ANTHROPIC_API_KEY=sk-ant-...

Switching providers takes two variables. Ollama needs two more, because a local model costs nothing per token and the built-in price catalog has no rate for it:

AI_CODE_PROVIDER=ollama
AI_CODE_MODEL=deepseek-coder-v2
AI_CODE_PRICE_INPUT=0
AI_CODE_PRICE_OUTPUT=0

Commit or stash your work before the first session. The agent edits files in place unless you turn on worktree mode, and git checkout -- . is only a safe way out if the rest of your work is already committed.

The Coding Session

php artisan ai:code opens the REPL. Type a task, watch the tool calls stream past, and read the git diff --stat that Laravel Prompts renders as a note block after each turn.

Plan mode is the flag worth knowing first. With --plan, a read-only planning agent investigates the codebase and streams a numbered plan covering files, changes, and risks before anything is written. You then pick Execute, Revise, or Cancel. The same thing works per task inside the session:

> /plan expire abandoned carts on a nightly schedule

Shell access has four modes, settable per session without touching config:

php artisan ai:code --off # RunShell refuses everything
php artisan ai:code --approve # confirm every command (the default)
php artisan ai:code --allowlist # only commands in shell_allowlist
php artisan ai:code --yolo # no prompts at all

--off is the one to use for questions. It leaves the read tools available so you can ask how authentication works in the app without the agent being able to run anything.

Transcripts are saved to storage/ai-code/ and resumed on the next run, and --session=search-rewrite keeps separate histories for separate streams of work. When a conversation passes 60,000 characters, Tackle automatically summarises the older exchanges and keeps the last four messages verbatim. /compact forces it, and /clear discards the history.

Safety Boundaries

Everything in config/tackle.php is checked before a tool runs, and no instruction in a prompt can override it:

return [
'budget_usd' => env('AI_CODE_BUDGET', 1.00),
 
'shell' => [
'local' => env('AI_CODE_SHELL', 'approve'),
'staging' => env('AI_CODE_SHELL', 'approve'),
'production' => env('AI_CODE_SHELL', 'off'),
],
 
'artisan_allowlist' => [
'local' => ['make:*', 'migrate:*', 'db:seed', 'route:list', 'test'],
'staging' => ['migrate', 'route:list'],
'production' => ['route:list'],
],
 
'worktree' => [
'local' => env('AI_CODE_WORKTREE', false),
'staging' => env('AI_CODE_WORKTREE', false),
'production' => env('AI_CODE_WORKTREE', true),
],
 
'protected_paths' => ['.env', '.env.*', 'storage/*', 'vendor/*', '.git/*'],
];

protected_paths blocks reads as well as writes, so the agent cannot see your .env to quote it back. Artisan commands in neither the allowlist nor artisan_destructive are refused outright, and the destructive list (migrate:fresh, db:wipe, and friends) requires a terminal confirmation. The budget is a hard stop: the session aborts when estimated spend crosses it, and a warning appears at 80%.

Worktree mode covers the case where you want to read a diff before anything reaches your working tree. Edits land in a temp git worktree, the session header shows worktree: on, and the per-turn diff stat is labelled "Worktree changes (live files untouched)". Worktrees are cleaned up when the session ends, and tackle:prune clears any left behind by an interrupted run.

Project Instructions

Every agent loads a TACKLE.md from the project root at session start. php artisan tackle:init scans composer.json, your test framework, and your app/ structure, then writes a scaffold with Conventions, Boundaries, and Gotchas headings for you to fill in:

## Conventions
 
- Timestamps are stored in UTC and cast with `immutable_datetime`.
- Queued jobs must be idempotent, because workers retry three times.
 
## Gotchas
 
- `Order::open()` excludes anything in the `pending_review` state.
- The `reporting` connection is a read replica, so writes against it throw.

If there is no TACKLE.md, Tackle falls back to AGENTS.md and then CLAUDE.md, so instructions you already keep for other tools work without a second file. Content is capped at 20,000 characters to protect your context window and your session budget, and the safety layer applies regardless of what the file says.

Reviewing Pull Requests

ai:review runs a read-only agent over a diff. It has ReadFile, Glob, and SearchCode but no editing tools, and it reads the full file around every changed function before commenting, so findings are grounded in more than the diff:

php artisan ai:review # staged and unstaged
php artisan ai:review --against=main # PR-style
php artisan ai:review --pr=118 --comment # post inline comments to a GitHub PR

Findings come back grouped by file at three severity levels, ending with a verdict of LGTM, LGTM with minor notes, or Needs changes. --fail-on=critical turns that verdict into an exit code, which is what lets a workflow block a merge.

Re-running against a pull request does not repeat the whole review. Each posted review embeds an invisible marker recording the head commit it covered; the next run compares against it, reviews only what was pushed since, tells the agent what it already reported, and exits early with "Nothing new to review" if the head hasn't moved. A force-push that loses the old commit falls back to a full review, and --full forces one on demand.

php artisan tackle:install review scaffolds the workflow for you:

name: Tackle Review
on: pull_request
 
permissions:
contents: read
pull-requests: write
 
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: JordanDalton/tackle-review@v1
with:
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
fail-on: critical

When a reviewer replies to a finding with a /tackle comment, ai:respond loads the comment and its thread, runs the coding agent against the instruction, pushes the commit to the PR branch, and replies in the thread with the SHA and diff stat. Pull requests from forks are refused in PHP, and a checkout that does not match the PR head aborts before the agent starts. If the comment asks a question rather than requesting a change, the agent answers and edits nothing. AI-assisted review is not new on its own, but taking the reply and pushing a commit from it is less common.

Self-Healing Queue Jobs

Self-healing is off by default. Set AI_CODE_HEALING_ENABLED=true, publish and run the migration, and start a worker on the healer queue:

php artisan vendor:publish --tag="tackle-migrations"
php artisan migrate
php artisan queue:work --queue=healer

From then on, a failed job fires Laravel's JobFailed event, Tackle's listener dispatches a HealJobFailure job to that dedicated queue, and a healing agent runs against a fresh worktree on a tackle/heal-{id} branch. It gets the exception class, message, and stack trace, plus the full entry from Telescope when it is installed. The agent applies a minimal fix, runs your test suite, and then either opens a pull request with its reasoning as the description or, in patch mode, merges into your working branch and re-dispatches the original job. Failed scheduled tasks go through the same path via ScheduledTaskFailed.

Some jobs shouldn't be auto-patched. An attribute keeps the healer away from them:

use Tackle\Attributes\Healable;
 
#[Healable(false)]
class IssueRefund implements ShouldQueue
{
public function handle(): void
{
// Skipped entirely, even with healing enabled.
}
}

The listener checks for it by reflection before dispatching anything. AI_CODE_HEALING_THRESHOLD=3 is the softer version of the same idea, holding off until a job has failed three times so transient failures get a chance to resolve on their own.

Every attempt is written to a tackle_healing_log table, whether it worked or not:

php artisan tackle:healing-log --type=job
php artisan tackle:healing-log --outcome=patched --limit=50

The table shows what failed, whether tests passed, the outcome, and a link to the PR or branch. In patch mode, a failing test run falls back to opening a pull request, so nothing merges unverified, and healer jobs run with $tries = 1 so a broken healer cannot loop.

Serving the Tools Over MCP

The Laravel-aware tools are useful outside Tackle's own agents. tackle:mcp serves them over MCP on stdio, so Claude Code, Cursor, or Zed can call ListRoutes, QueryDatabase, ReadTelescopeEntry, and RunLarastan against your app:

claude mcp add tackle -- php artisan tackle:mcp

The exposed set comes from config('tackle.mcp.tools') and defaults to read and analysis tools with no file writes and no shell. Path guards, the Artisan allowlist, and the SELECT-only database restriction still apply, because the tools enforce them regardless of what calls them. AskUser and ConfirmAction are refused outright because an MCP client has no terminal to answer their prompts.

Two limits are worth knowing before you install it. The agent has no internet access, so it works only from files in your workspace and cannot pull down a package's documentation. The spend limit is estimated from token counts against a built-in price catalogue, so what your provider actually bills will differ slightly. Tackle is MIT-licensed and currently at v1.27.3 on GitHub, with the full documentation at tackle.jordandalton.com.

Yannick Lyn Fatt photo

Staff Writer at Laravel News and Full stack web developer.

Sponsored

acquaintsoft logo
Acquaint Softtech

Hire Laravel developers with AI expertise at $20/hr. Get started in 48 hours.

Visit Acquaint Softtech

The latest

View all →
Queue::forward(): Reroute Laravel Queues in One Place image

Queue::forward(): Reroute Laravel Queues in One Place

Read article
Laravel Read-Through Filesystem: Lazy Storage Migration image

Laravel Read-Through Filesystem: Lazy Storage Migration

Read article
Read-Through Disks and Debounced Listeners in Laravel 13.26 image

Read-Through Disks and Debounced Listeners in Laravel 13.26

Read article
Lerd: A Free, Open Source Herd Alternative for Linux and macOS image

Lerd: A Free, Open Source Herd Alternative for Linux and macOS

Read article
Let's Encrypt HTTPS on an IP Address With FrankenPHP image

Let's Encrypt HTTPS on an IP Address With FrankenPHP

Read article
Laravel Chores: Resumable Data Operations and Cleanups image

Laravel Chores: Resumable Data Operations and Cleanups

Read article