Directory Write Permissions Analyzer
| Analyzer ID | Category | Severity | Time To Fix |
|---|---|---|---|
directory-write-permissions | ✅ Reliability | Critical | 10 minutes |
What This Checks
- Verifies that
storage/directory is writable - Verifies that
bootstrap/cache/directory is writable - Checks custom directories if configured (via published config file)
- Tests actual write permissions, not just existence
- Validates both relative and absolute paths from configuration
- Reports all failed directories with actionable fix commands
- Supports symlinked directories
- Verifies storage symlinks exist (from
config('filesystems.links')) — skipped automatically for API-only apps - Detects broken symlinks (link exists but target doesn't)
- Validates symlink targets are directories
- Default: checks
public/storage→storage/app/public
Why It Matters
- Application crashes: Laravel requires writable storage for logs, sessions, cache, and compiled views - without it, your app will fail
- Silent failures: Missing write permissions can cause intermittent errors that are hard to debug in production
- Security logs: Without writable storage, security events and errors won't be logged, hiding potential attacks
- Performance degradation: Cache and compiled views require write access - without it, your app runs significantly slower
- Session management: User sessions require writable storage - login/authentication will fail without it
- File uploads: User file uploads to
storage/will fail silently or with cryptic errors - Deployment issues: Fresh deployments often fail due to incorrect directory permissions, especially in Docker/CI environments
- Broken file URLs: Without the storage symlink, publicly accessible files in
storage/app/publicreturn 404 errors - Image/file display: User-uploaded images and files won't display without proper symlinks
- Deployment failures: New deployments often forget to recreate symlinks after fresh clones
How to Fix
Quick Fix (5 minutes)
- Identify which directories are not writable:
# Check current permissions
ls -la storage/
ls -la bootstrap/cache/- Fix permissions based on your environment:
Development (Local Machine):
# Make directories writable
chmod -R 775 storage bootstrap/cacheProduction (Linux Server with www-data user):
# Set correct owner and permissions
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cacheDocker Container:
# In your Dockerfile
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
RUN chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cacheWindows:
# Right-click folders → Properties → Security tab
# Grant "Full Control" to your web server user
# Or run as Administrator:
icacls storage /grant Users:F /t
icacls bootstrap\cache /grant Users:F /tSymlink Issues:
# Check if storage symlink exists
ls -la public/storage
# If missing or broken, recreate it
php artisan storage:link
# For custom symlinks, add to config/filesystems.php:
'links' => [
public_path('storage') => storage_path('app/public'),
public_path('uploads') => storage_path('app/uploads'),
],
# Then run:
php artisan storage:linkProper Fix (10 minutes)
- Configure deployment automation to set permissions:
# .github/workflows/deploy.yml
- name: Set Directory Permissions
run: |
chmod -R 775 storage bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache- Add post-deployment script:
#!/bin/bash
# deploy/fix-permissions.sh
# Set ownership
chown -R www-data:www-data storage bootstrap/cache
# Set directory permissions (775 = rwxrwxr-x)
chmod -R 775 storage bootstrap/cache
# Ensure new files inherit correct permissions
chmod g+s storage bootstrap/cache
echo "✓ Directory permissions configured"- Configure umask in your web server:
Nginx + PHP-FPM (/etc/php/8.1/fpm/pool.d/www.conf):
; Set umask so new files are group-writable
php_admin_value[umask] = 0002Apache (.htaccess or VirtualHost):
<IfModule mod_php.c>
php_value umask 0002
</IfModule>- Update your
.gitignoreto exclude storage files:
/storage/*.key
/storage/app/*
!/storage/app/.gitignore
/storage/framework/cache/*
!/storage/framework/cache/.gitignore
/storage/framework/sessions/*
!/storage/framework/sessions/.gitignore
/storage/framework/testing/*
!/storage/framework/testing/.gitignore
/storage/framework/views/*
!/storage/framework/views/.gitignore
/storage/logs/*
!/storage/logs/.gitignore- Configure custom writable directories (optional):
If you need to check additional directories beyond the defaults (storage and bootstrap/cache), publish the config:
php artisan vendor:publish --tag=shieldci-configThen in config/shieldci.php:
'analyzers' => [
'reliability' => [
'enabled' => true,
'directory-write-permissions' => [
'writable_directories' => [
'storage',
'bootstrap/cache',
'public/uploads', // If you store uploads here
'resources/compiled', // Custom compiled assets
],
],
],
],TIP
By default, the analyzer checks storage and bootstrap/cache directories. You only need to publish the config if you want to check additional directories.
- Include symlink creation in deployment:
# .github/workflows/deploy.yml
- name: Create Storage Symlinks
run: php artisan storage:link --force# deploy/post-deploy.sh
php artisan storage:link --force
echo "✓ Storage symlinks created"WARNING
The --force flag recreates symlinks even if they exist. Use with caution in production if you have custom symlink configurations.
- Configure symlink checking (optional):
If you want to disable symlink verification or check custom symlinks:
// config/shieldci.php
'analyzers' => [
'reliability' => [
'enabled' => true,
'directory-write-permissions' => [
'check_symlinks' => true, // Set to false to disable
],
],
],
// Or use Laravel's filesystems config:
// config/filesystems.php
'links' => [
public_path('storage') => storage_path('app/public'),
public_path('media') => storage_path('app/media'),
],ShieldCI Configuration
This analyzer is automatically skipped in CI environments ($runInCI = false).
Why skip in CI?
- CI runners clone a fresh repository and never run
php artisan storage:link, so thepublic/storagesymlink is always absent - Directory write permissions depend on the CI runner's OS and file system setup
- Prevents misleading failures in pipelines where the deployment steps that create symlinks and set permissions have not yet run
Laravel Vapor / Serverless: This analyzer is automatically skipped on Laravel Vapor and other serverless platforms — directory write permissions are managed by the platform and cannot be changed by the user.
API-only / Stateless applications: Symlink checks are additionally skipped for API-only applications — symlinks are web-specific and not meaningful when the app serves no browser requests. Write-permission checks for storage/ and bootstrap/cache/ still run.
When to run this analyzer:
- ✅ Local development: Catches missing write permissions and broken symlinks before they cause runtime errors
- ✅ Staging/Production servers: Validates that storage directories are writable and symlinks are in place after deployment
- ❌ CI/CD pipelines: Skipped automatically (symlinks not created and permissions not set in CI)
- ❌ Laravel Vapor / Serverless: Skipped automatically (platform manages permissions)
References
- Laravel Installation - Directory Permissions
- Laravel File Storage - The Public Disk
- Linux File Permissions Guide
- Docker Security Best Practices
- Nginx + PHP-FPM Configuration
Related Analyzers
- Environment File Existence Analyzer - Ensures .env file exists and is readable
- Cache Status Analyzer - Validates cache connectivity and functionality