A small PHP development tool for removing configurable development-only metadata from composer.json files when
releasing packages for distribution.
A package's composer.json often contains metadata that is useful while developing and maintaining the package, but is
not needed by downstream consumers.
For example, development dependencies, development autoloading, and Composer scripts can make up a significant part of a project's development manifest.
composer-peel lets you explicitly define which Composer sections should be removed from a release manifest.
Keep the release package manifest focused on what consumers need and leave development metadata behind.
Removing require-dev from a release manifest does not imply lower package quality. Development dependencies describe
how a package is tested and maintained, not what consumers need to run it. The complete development setup remains
in the source repository; composer-peel simply keeps those development concerns out of the distributed package.
The sections removed by composer-peel are configurable. They are removed from the release composer.json; they are
not permanently removed from the source repository's development configuration.
The default configuration removes:
| Section | Purpose |
|---|---|
require-dev |
Development-only dependencies |
autoload-dev |
Development-only autoloading |
scripts |
Composer scripts used during development and CI |
scripts-descriptions |
Composer script descriptions |
scripts-aliases |
Composer script aliases |
Runtime dependencies and package autoloading remain untouched.
Even widely used packages can carry significant development metadata into their published composer.json.
For example, peeling the require-dev section from symfony/console v8.1.8 would reduce its manifest from
1,813 bytes to 1,170 bytes — a saving of 643 bytes (35.5%).
With more than 1.2 billion installations, that amounts to a theoretical cumulative saving of approximately 782 GB of Composer metadata.
The example illustrates the principle behind composer-peel: small savings in a package manifest can become
significant at scale.
Install composer-peel as a development dependency:
composer require --dev stolt/composer-peelRemove the configured sections from the current composer.json:
composer-peel peelPreview the changes without modifying the manifest:
composer-peel peel --dry-runUse a custom configuration:
composer-peel peel --config=.composer-peel.phpCreate a backup before modifying the manifest:
composer-peel peel --backup-file=.composer-unpeeled.jsonThe init command generates a .composer-peel.php file from the current internal defaults. This is useful when you
want the peeling policy to be explicit and version-controlled:
composer-peel initAn existing configuration is not overwritten by default:
composer-peel init --overwriteThe --dry-run option shows:
- the manifest being processed;
- the configuration being used;
- the sections that would be removed;
- the original manifest size;
- the projected manifest size;
- the estimated size reduction.
For example:
$ composer-peel peel --dry-run
Manifest: composer.json
Configuration: internal defaults
Sections to be removed:
- require-dev
- autoload-dev
- scripts
- scripts-descriptions
- scripts-aliases
Original size: 2,480 bytes
Projected size: 1,120 bytes
Estimated reduction: 1,360 bytes (54.8%)
Dry run completed. No files were modified.
The values above are illustrative; the actual result depends on the contents of composer.json and the configured
peeling rules.
To see the exact structural changes:
composer-peel peel --dry-run --diffFor machine-readable output:
composer-peel peel --dry-run --format=jsonWhen --format=json and --diff are combined, the JSON output contains an additional diff key with the unified diff.
Use status to get a read-only overview of the current Composer Peel state and determine whether the
repository is ready for the release workflow:
composer-peel statusThe command inspects:
- the current Composer manifest state (peeled, unpeeled, or modified);
- whether a Composer backup exists;
- the optional Composer package version (separate from the application version);
- discovered application version sources and their consistency;
- the latest Git tag;
- the CHANGELOG version;
- the working tree state;
- overall release readiness.
The status command distinguishes between two version concepts:
- Package version: the optional
versionfield incomposer.json. This is metadata for the Composer package and is not used as an application version source. - Application version: discovered from supported source files such as
bin/files,src/Console/Application.php,src/Application.php, andsrc/Server.php. This follows the same detection rules asversion-aligner.
composer.json is never used as an application version source.
For a healthy, release-ready state:
Composer Peel status
Composer manifest peeled
Backup available
Package version 1.4.0
Application versions
bin/composer-peel 1.4.0
src/Console/Application.php 1.4.0
Application version 1.4.0
Version consistency consistent
Latest Git tag v1.4.0
CHANGELOG.md 1.4.0
Working tree clean
Release state ready
For an inconsistent state:
Composer Peel status
Composer manifest peeled
Backup available
Application versions
bin/composer-peel 1.3.0
src/Console/Application.php 1.4.0
Application version 1.4.0
Version consistency inconsistent
Latest Git tag v1.3.0
CHANGELOG.md 1.4.0
Working tree clean
Release state not ready
Issues:
- bin/composer-peel contains version 1.3.0 but the other application version source contains 1.4.0
- Application version 1.4.0 does not match the latest Git tag v1.3.0
The command exits with 0 when the repository is release-ready, and with a non-zero exit code when issues are
detected. It is safe to run repeatedly and does not modify any files, Git state, commits, or tags.
A custom configuration file can be specified:
composer-peel status --config=.composer-peel.phpUse validate to verify the peeled composer.json before releasing it:
composer-peel validateThe following checks are performed:
composer.jsoncontains a valid JSON object;- the backup file exists when backups are enabled;
- the backup file contains a valid JSON object;
- none of the configured sections is still present in
composer.json; - the required runtime sections
nameandrequirestill exist; - all other sections are unchanged compared with the backup;
composer validate --no-check-lockreports no errors forcomposer.jsonand the backup.
The command exits with a non-zero status code if a check fails, making it suitable for CI. Checks that depend on the backup are skipped when backups are disabled.
The Composer validation checks require the composer binary to be available on PATH. Warnings do not fail validation,
but errors — including publish errors such as a missing description — do.
The lock file is not checked because it is expected to retain the development dependencies removed from the release manifest.
To skip the Composer validation checks:
composer-peel validate --skip-composer-validateA custom backup or configuration file can also be specified:
composer-peel validate --backup-file=my-backup.json --config=.composer-peel.phpWhen peel has created a backup, rollback restores the original composer.json:
composer-peel rollbackBy default, the backup file is deleted after a successful rollback. Keep it with:
composer-peel rollback --keep-backupRestore and commit the development manifest immediately:
composer-peel rollback --commitThe commit uses the configured rollback commit message. Override it for a single invocation with:
composer-peel rollback --commit --commit-message="chore: start next development cycle"--commit-message requires --commit.
The --commit option can be combined with --keep-backup. If committing fails, the command exits with an error and
keeps the backup so the rollback can be retried.
Before restoring, rollback verifies that composer.json is still the peeled version of the backup. If the manifest
has been modified since it was peeled, the rollback is aborted to avoid discarding those changes.
To restore the backup anyway:
composer-peel rollback --forceIf composer.json already matches the backup, it is left untouched.
Use --dry-run to preview the rollback checks, the commit message, and whether the backup would be removed:
composer-peel rollback --dry-run
composer-peel rollback --commit --dry-runCustom backup and configuration files can also be supplied:
composer-peel rollback --backup-file=my-backup.json --config=.composer-peel.phpcomposer-peel supports an optional PHP configuration file named .composer-peel.php in the project root.
The configuration can define:
- which Composer sections are peeled;
- the release backup;
- which files may be included in a release commit;
- release and rollback commit messages.
<?php
declare(strict_types=1);
return [
'peel' => [
'sections' => [
'require-dev',
'autoload-dev',
'scripts',
'scripts-descriptions',
'scripts-aliases',
],
],
'release' => [
'backup' => [
'enabled' => true,
'path' => '.composer-unpeeled.json',
],
'files' => [
'CHANGELOG.md',
'bin/',
],
'commit_message' => 'chore: release version {{version}}',
],
'rollback' => [
'commit_message' => 'chore: restore development Composer manifest',
],
];peel.sections defines the top-level Composer sections that should be removed:
'peel' => [
'sections' => [
'require-dev',
'autoload-dev',
'scripts',
'scripts-descriptions',
'scripts-aliases',
],
],Only explicitly configured sections are peeled. This makes the behavior predictable and lets each package decide which metadata belongs exclusively to its development workflow.
Note
composer-peel only permits stripping development-oriented metadata. Attempting to configure protected
sections such as require or autoload causes the operation to fail.
The release backup stores the original composer.json before it is peeled:
'release' => [
'backup' => [
'enabled' => true,
'path' => '.composer-unpeeled.json',
],
],The backup provides the source for restoring the development manifest if the release process fails before restoration.
Make sure the backup file is not unintentionally included in the release.
By default, the release workflow allows the peeled composer.json, the backup, CHANGELOG.md, and modified files in
bin/ to be committed.
The default CHANGELOG.md and bin/ allowances can be replaced through release.files:
'release' => [
'files' => [
'CHANGELOG.md',
'bin/',
],
],Directories must end with /.
The release workflow uses separate commit messages for the peeled release manifest and the restored development manifest:
'release' => [
'commit_message' => 'chore: release version {{version}}',
],
'rollback' => [
'commit_message' => 'chore: restore development Composer manifest',
],release.commit_message is used for the commit containing the peeled manifest. The {{version}} placeholder is
replaced with the release tag.
rollback.commit_message is used by rollback --commit when restoring the development manifest.
Both can be overridden for an individual invocation with --commit-message.
The release command commits and tags an already peeled composer.json. It does not peel the manifest itself.
A complete release workflow is:
composer-peel peel
composer-peel validate
composer-peel release v1.0.0
composer-peel rollback --commitThe workflow is:
- Run
peelto remove the configured development-only sections and create the backup. - Optionally run
validateto inspect the peeled manifest independently. - Run
releaseto commit the peeled manifest and create the Git tag. - Run
rollback --committo restore the original developmentcomposer.jsonand commit the restoration.
Before proceeding, release verifies that:
- the requested tag is a valid Semantic Version, such as
v1.0.0; - the tag does not already exist;
- the tag is strictly greater than the latest released version;
- the working tree is in the expected release state;
- the backup exists;
composer.jsonis exactly the backup with the configured sections removed;- only allowed files have changed.
By default, the allowed release changes are the peeled composer.json, the backup, CHANGELOG.md, and modified files
in bin/.
If the release state is invalid, no commit or tag is created.
To override the release commit message:
composer-peel release v1.0.0 --commit-message="chore: release {{version}}"To preview the files, commit message, and tag without creating them:
composer-peel release v1.0.0 --dry-runThe release tag points to the commit containing the peeled manifest. After rollback --commit, the development branch
contains the restored manifest:
Development commit
│
▼
Peeled manifest commit ──► Release tag (v4.2.1)
│
▼
Restored development manifest commit
Important
The release tag points to a commit containing a different composer.json from the development branch.
The automated release workflow only works when the peeled-manifest commit does not need to pass the project's normal development CI checks. After peeling, development dependencies, development autoloading, and Composer scripts are no longer available.
If your release process requires CI validation of the tagged commit, consider using composer-peel as a separate
distribution/build step instead of tagging the peeled manifest directly.
composer-peel modifies only composer.json. It does not modify composer.lock.
composer-peel includes a repository-local AI skill at .agents/skills/composer-peel/SKILL.md.
The skill teaches compatible coding agents how to inspect the configuration, preview changes, peel the configured sections, and safely perform the release workflow.
This CLI and its library are licensed under the MIT license. Please see LICENSE.md for more details.
All noteworthy changes are documented in CHANGELOG.md.
If you're considering contributing to this project, have a look at this repository's CONTRIBUTING.md for more advice.
