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:codeis the interactive session: a REPL with plan mode, slash commands, image attachments, and history that survives between runsai:runis the same agent with no terminal attached: one task, an exit code, and--output=jsonwhen a pipeline needs to read the resultai:fixstarts a focused fix session from a pasted exception, a Sentry issue (--sentry=ID), or a GitHub issue (--issue=N)ai:reviewreads a diff or a pull request and posts findings as inline review comments with severity levelsai:upgradetakes a Composer package across a major version, working from the package's own upgrade guide invendor/- 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-tacklephp 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=ollamaAI_CODE_MODEL=deepseek-coder-v2AI_CODE_PRICE_INPUT=0AI_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 everythingphp artisan ai:code --approve # confirm every command (the default)php artisan ai:code --allowlist # only commands in shell_allowlistphp 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 unstagedphp artisan ai:review --against=main # PR-stylephp 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 Reviewon: 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 migratephp 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=jobphp 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.