A Claude Code skill that adds a secure deploy webhook endpoint to any Laravel + frontend project.
One command in Claude Code and you get a production-ready endpoint that:
- Pulls from GitHub
- Detects which files changed (
git diff) - Conditionally runs only what's needed:
composer update,migrate,npm buildper frontend - Protects itself with IP throttle + 7-day ban + token validation
/plugin add maxirodr/fullstack-deploy-webhook
In any Laravel project, tell Claude:
add a deploy webhook
Or use the slash command:
/fullstack-deploy-webhook
Claude will automatically:
- Detect your Laravel app and any frontend directories
- Create a
DeployControllerwith the full deploy logic - Add the route, config values, and
.env.exampleentries - Give you the production setup instructions
| File | Action |
|---|---|
app/Http/Controllers/Api/V1/DeployController.php |
Created |
routes/api.php |
Modified (adds /api/deploy route) |
config/app.php |
Modified (adds deploy_token, deploy_git_user, deploy_git_repo) |
.env.example |
Modified (adds DEPLOY_TOKEN, DEPLOY_GIT_USER, DEPLOY_GIT_REPO) |
my-project/
backend/ <- Laravel app
frontend/ <- React, Vue, etc.
landing/ <- Next.js, Astro, etc.
my-project/ <- Laravel app
frontend/ <- React, Vue, etc.
The skill auto-detects the structure and adjusts paths accordingly.
The endpoint compares HEAD before and after git pull, then only runs steps for directories that actually changed:
| Change detected in | Action |
|---|---|
composer.json / composer.lock |
composer update --no-dev |
database/migrations/ |
php artisan migrate --force |
frontend/ |
npm install + npm run build |
landing/ |
npm install + npm run build |
| Nothing changed | Returns no_changes, skips everything |
Laravel cache is always cleared (optimize:clear) since it's fast and safe.
- Token validation via
DEPLOY_TOKENenv variable - GitHub PAT passed per-request (not stored on server)
- IP throttle: 5 failed attempts per hour
- IP ban: 7 days after exceeding throttle
- Logging: All deploy activity logged via Laravel Log
These are issues we hit in production and solved in the template:
| Problem | Solution in the skill |
|---|---|
| Git "unsafe directory" error (www-data vs root) | git -c safe.directory=X with realpath() |
Symfony Process -c flag silently fails |
Splits -c and value as separate array elements |
tsc/vite/next "command not found" |
Adds node_modules/.bin to Process PATH |
artisan migrate hangs in production |
Uses --force flag |
| Frontend build times out | 300s timeout (default is 60s) |
After Claude creates the endpoint, you need to configure your server:
openssl rand -hex 32DEPLOY_TOKEN=<the-token-you-generated>
DEPLOY_GIT_USER=your-github-username
DEPLOY_GIT_REPO=your-username/your-reposudo chown -R www-data:www-data /var/www/your-projectThe web server (Apache/Nginx) runs as www-data. Without this, git and PHP will have permission issues.
Go to GitHub Settings > Tokens and create a classic token with repo scope.
GET https://your-domain.com/api/deploy?token=YOUR_DEPLOY_TOKEN&git_token=YOUR_GITHUB_PAT
You can call this from:
- A browser bookmark
- A GitHub Actions workflow
- A cURL script
- Any HTTP client
{
"status": "completed",
"timestamp": "2025-01-15T10:30:00+00:00",
"results": {
"git_pull": { "success": true, "output": "Updating abc123..def456" },
"changed_files": ["frontend/src/App.tsx", "frontend/package.json"],
"composer_update": { "skipped": true, "reason": "no changes in composer.*" },
"artisan_migrate": { "skipped": true, "reason": "no new migrations" },
"artisan_optimize_clear": { "success": true },
"frontend_install": { "success": true },
"frontend_build": { "success": true }
}
}Your IP exceeded 5 failed attempts. The ban lasts 7 days. To unban manually:
php artisan tinker
>>> Cache::forget('deploy_banned:YOUR_IP');Common causes:
- Wrong
DEPLOY_GIT_USERorDEPLOY_GIT_REPOin.env - GitHub PAT expired or missing
reposcope - Merge conflicts on the server (fix manually with
git reset --hard origin/main)
The node_modules/.bin PATH fix should handle this. If it persists:
- Verify
npm installran successfully (checknode_modules/exists) - Check the build script in
package.jsonisn't using a global binary
Increase the timeout in the runCommand call for the build step. Default is 300s (5 min). For very large Next.js projects on small VPS, you may need 600s.
MIT