laravel/chisel
Laravel Chisel provides primitives for starter-kit post-install scripts, letting users opt into features and automatically remove unwanted code. Define a chisel.php with questions and file/PHP mutations to prune sections, imports, interfaces, and config.
Start by installing Chisel via Composer:
composer require laravel/chisel
For your first use case, create a chisel.php script in your project root. Define optional features using Question::multiselect() and specify file mutations for each selection. Example:
return Chisel::script(dirname(__DIR__))
->questions([
Question::multiselect(
name: 'features',
label: 'Select features to enable:',
options: [
'auth' => 'Authentication',
'api' => 'API Support',
],
),
])
->selected('features', 'auth', then: fn($c) => $c->apply(fn($c) => $c->php('User.php')->removeInterface('MustVerifyEmail')))
->selected('features', 'api', then: fn($c) => $c->apply(fn($c) => $c->file('routes/web.php')->removeLinesContaining('Route::get'))));
Run the script via an Artisan command (see Implementation Patterns for details).
Where to look first:
chisel.php template structureQuestion::multiselect() in chisel.php to present feature choices.selected(), selectedAny(), or selectedAll() to handle conditional mutations.removeSection(), removeImport(), or delete().Laravel\Prompts for interactive prompts.->selected('features', 'auth', then: fn($c) => $c->apply(
fn($c) => $c->php('User.php')
->removeInterface('MustVerifyEmail')
->removeTrait('Notifiable')
))
Wrap optional code in markers (PHP/JSX):
/* @chisel-passkeys */
Fortify::authenticateUsingPasskeys();
/* @end-chisel-passkeys */
Remove with:
->selected('features', 'passkeys', else: fn($c) => $c->files('FortifyServiceProvider.php')->removeSection('passkeys'))
->selected('features', 'api', then: fn($c) => $c->apply(
fn($c) => $c->npm()->remove('laravel-sanctum')
))
use Laravel\Prompts\multiselect;
public function handle(): void {
$script = require base_path('chisel.php');
$answers = $script->collectAnswers()
->onQuestion(fn(Question $q) => multiselect(
label: $q->label,
options: $q->options,
default: $q->default ?? [],
))
->interactive($this->input->isInteractive())
->withAnswers(json_decode($this->option('answers'), true) ?? []);
$script->chisel($answers);
$this->info('Chisel cleanup complete!');
}
post-install-cmd to composer.json:
"scripts": {
"post-install-cmd": "php artisan install:features"
}
--answers flag to skip prompts:
php artisan install:features --answers='{"features":["auth"]}'
collectAnswers() to test mutations:
$script->chisel(['features' => ['auth']]);
Section Marker Mismatches:
@chisel-* markers are balanced (e.g., /* @chisel-x */ ... /* @end-chisel-x */).{ /* @chisel-x */ } syntax, not HTML-style comments.removeSectionMarkers() to debug—it keeps content but removes markers.PHP AST Limitations:
removeImport() only works for fully qualified class names (e.g., Illuminate\Support\Facades\Log).use App\Models\User) may fail.npm Detection:
npm, yarn, pnpm, or bun via which.npm()->install() throws.$c->npm()->setManager('yarn')->install();
Interactive Mode Quirks:
withAnswers() overrides interactive prompts completely. Missing keys fall back to defaults, not prompts.$answers = $script->collectAnswers()->withAnswers($providedAnswers)->toArray();
File Paths:
dirname(__DIR__)).base_path() for clarity.$script->apply(fn($c) => $c->php('User.php')->removeInterface('MustVerifyEmail'))
->then(fn($mutations) => $this->info('Would run: ' . print_r($mutations, true)));
removeSectionMarkers() to inspect content:
$c->file('FortifyServiceProvider.php')->removeSectionMarkers('passkeys');
try-catch:
try {
$c->php('User.php')->removeInterface('MustVerifyEmail');
} catch (\Exception $e) {
$this->error("Failed to remove interface: " . $e->getMessage());
}
Custom Question Types:
Extend Laravel\Chisel\Question for checkboxes, radio buttons, etc. Example:
Question::checkbox('features', 'Enable API?', ['api' => 'API Support']);
Register a handler in collectAnswers()->onQuestion().
Custom Mutations:
Implement Laravel\Chisel\Mutation for domain-specific edits (e.g., database migrations):
$c->custom('migrations', fn($m) => $m->dropTableIfExists('failed_jobs'));
Pre/Post Hooks:
Use apply() for global mutations (e.g., .env cleanup):
$script->apply(fn($c) => $c->file('.env')->replace('APP_DEBUG=true', 'APP_DEBUG=false'));
Package Manager Extensions:
Override npm() detection logic in a service provider:
Chisel::extend('npm', fn() => new CustomNpmManager());
Question constructor:
Question::multiselect(..., default: ['auth'])
required: true in Question:
Question::multiselect(..., required: true)
hint for multi-line instructions:
Question::multiselect(..., hint: 'Select all features you need.\nUse arrow keys to navigate.')
How can I help you explore Laravel packages today?