Foxy reads project options from config.foxy in the root composer.json:
{
"config": {
"foxy": {
"manager": "npm"
}
}
}Values are resolved in this order, from highest to lowest priority:
- Environment variables beginning with
FOXY__. - The project
composer.jsonfile. <COMPOSER_HOME>/config.json.<COMPOSER_HOME>/composer.json.- Foxy defaults.
Global values must be placed under config.foxy in one of the two Composer home files. Composer does not accept
arbitrary nested options through composer global config, but composer global config --editor can be used to edit
the global file directly.
Convert an option name to uppercase, replace hyphens with underscores, and prefix it with FOXY__. For example,
run-asset-manager becomes FOXY__RUN_ASSET_MANAGER.
Foxy accepts strings, integers, case-insensitive boolean values (true, false, yes, no, y, n, 1, and
0), and JSON arrays or objects.
FOXY__ENABLED=false composer install
FOXY__MANAGER_TIMEOUT=420 composer install
FOXY__ENABLE_PACKAGES='{"foo/*":true}' composer installUse shell-appropriate quoting when passing JSON. A manager-specific map is supported in Composer configuration, but a manager-prefixed environment variable should contain the scalar value for the active manager.
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enables Foxy processing. |
manager |
string or null |
null |
Selects bun, deno, native, npm, pnpm, or yarn; null means automatic. |
manager-version |
string or map | Empty | Adds a constraint within the built-in supported manager range. |
manager-bin |
string or map | Manager executable | Overrides the manager executable. |
manager-options |
string or map | Empty | Appends options to both install and update commands. |
manager-install-options |
string or map | Empty | Appends options only to install commands. |
manager-update-options |
string or map | Empty | Appends options only to update commands. |
manager-timeout |
integer or map | No practical limit | Sets the manager process timeout in seconds. |
run-asset-manager |
boolean | true |
Controls automatic manager probing and install or update execution. |
fallback-asset |
boolean | true |
Restores package.json after asset processing fails. |
fallback-composer |
boolean | true |
Restores Composer lock and vendor state after asset solving fails. |
composer-asset-dir |
string or null |
<vendor-dir>/php-forge/composer-asset/ |
Sets the mock package directory. |
enable-packages |
array or object | [] |
Includes or excludes Composer packages by pattern. |
root-package-json-dir |
string or null |
Project or package root | Sets the directory containing package.json. |
registry-url |
string | https://registry.npmjs.org |
Sets the npm registry used by the native manager. |
native-install-dir |
string | node_modules |
Sets the directory the native manager installs into. |
Set enabled to false to skip manager discovery, fallback snapshots, package merging, and manager execution:
{
"config": {
"foxy": {
"enabled": false
}
}
}Set the manager explicitly when local development and CI must always use the same tool:
{
"config": {
"foxy": {
"manager": "pnpm"
}
}
}When manager is null, Foxy looks for one recognized lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock,
bun.lock, deno.lock, or foxy.lock). Multiple recognized lockfiles require explicit selection. Without a lockfile,
available executables are considered in this order: npm, pnpm, Yarn, Bun, and Deno. The native manager is never
selected by availability; it is used only when manager is native or when foxy.lock is the single recognized
lockfile. Commit the lockfile generated by the selected manager. Foxy reports an error when an explicitly configured
manager is unknown, or when execution is enabled and its executable is unavailable.
Foxy resolves the manager when the first install, update, or audit needs it, not when Composer loads the plugin, so
Composer commands that do none of these (for example about, show, or validate) never probe an executable.
Selection errors are therefore reported by the first command that needs the manager.
When run-asset-manager is false, automatic selection does not probe executables. Foxy uses the manager identified
by a single recognized lockfile, or npm as the manifest adapter when no lockfile exists. Multiple lockfiles still
require an explicit manager value.
When manager execution is enabled, Foxy validates the selected manager against its built-in supported constraint:
| Manager | Built-in constraint |
|---|---|
| Bun | ^1.4.0 |
| Deno | ^2.9.7 |
| npm | >=10.9.8 |
| pnpm | ^11.23.0 |
| Yarn | ^4.18.0 |
The native manager has no binary and no version constraint; manager-version, manager-bin, manager-options,
manager-install-options, manager-update-options, and manager-timeout do not apply to it.
The manager-version option adds another Composer constraint that is evaluated together with the built-in constraint.
It can narrow the accepted versions for a project, but it cannot replace or widen Foxy's supported range.
Foxy treats the reported value as one concrete release and validates it from root-package-json-dir before every
manager command. During automatic Composer processing, run-asset-manager=false prevents probing, execution, and
validation of the manager binary. An explicit composer foxy:audit remains an exception because it is a direct user
request.
Narrow the npm constraint for one project:
{
"config": {
"foxy": {
"manager": "npm",
"manager-version": "~10.9.8"
}
}
}Manager-prefixed options may also use a map when a shared configuration supports several managers:
{
"config": {
"foxy": {
"manager-version": {
"npm": "~10.9.8",
"pnpm": "~11.23.0"
}
}
}
}For example, configuring npm with <10.9.8 does not enable an older release because the built-in >=10.9.8
constraint remains in force. Remove manager-version to accept the complete built-in range for the selected manager.
Use manager-bin for an explicit executable path:
{
"config": {
"foxy": {
"manager": "npm",
"manager-bin": "/opt/node/bin/npm"
}
}
}Use the three manager option settings only when native manager configuration files cannot express the requirement:
{
"config": {
"foxy": {
"manager": "npm",
"manager-options": "--no-audit",
"manager-install-options": "--ignore-scripts",
"manager-update-options": "--save"
}
}
}These values are appended to an external command. Treat project, global, and environment configuration as trusted
input. Prefer native files such as .npmrc, .yarnrc.yml, pnpm-workspace.yaml, or bunfig.toml where appropriate.
They apply to install and update operations only. foxy:audit owns its machine-output, advisory-filter, and
dependency-scope flags and does not inherit these options. It does honor manager-bin, manager-version,
manager-timeout, and root-package-json-dir.
Where the native manager supports an explicit override, Foxy neutralizes inherited settings that could exclude
dependencies or advisories. Bun 1.4 cannot reset every inherited dependency-scope setting without also discarding
registry configuration. Foxy therefore rejects a Bun audit when a loaded .npmrc or bunfig.toml excludes a dependency
type required by the requested audit. Remove the restrictive setting, or use --no-dev when the only restriction is the
development dependency graph. Audit preflight also requires UTF-8 configuration and canonical [install] table syntax;
inline or array install tables, escaped keys or omit values, and multiline values inside [install] are rejected rather
than interpreted heuristically. The preflight rejects restrictive declarations even when a later include or
higher-precedence file would override them.
Deno installs a local file: dependency without installing that package's own dependencies. Foxy therefore also
registers each Composer asset directory as a member of the workspaces list in the root package.json:
{
"dependencies": {
"@composer-asset/acme--theme": "file:./vendor/php-forge/composer-asset/acme/theme"
},
"workspaces": ["packages/*", "vendor/php-forge/composer-asset/acme/theme"]
}- Existing
workspacesentries are preserved and kept before the Foxy-managed members. Foxy removes a member when its Composer asset is removed, and removes theworkspacesfield when no entry remains. workspacesmust be a list of strings. The object form with apackageskey is rejected.- Composer assets must be located in a subdirectory of the root package directory because Deno rejects workspace
members outside it. Foxy writes each member as a normalized path relative to that directory. A
root-package-json-dirthat excludes the Composer vendor directory, or acomposer-asset-diroutside the root package directory, is not supported with Deno. - Updates run
deno update --lockfile-only --recursive && deno install. The first step updatesdeno.lockwithin the declared version ranges, and the second installs the locked dependencies.manager-optionsapplies to both steps,manager-update-optionsto the update step, andmanager-install-optionsto the install step. - Deno skips npm lifecycle scripts by default. Allow them with
--allow-scriptsinmanager-install-options. composer foxy:auditreadsdeno.lockand parses the text report ofdeno audit.--no-devis rejected because Deno cannot exclude development dependencies. See Security auditing.- When switching from Deno to another manager, remove the Foxy-managed entries from
workspaces.
{
"config": {
"foxy": {
"manager": "deno",
"manager-install-options": "--allow-scripts"
}
}
}Set manager to native to install the frontend dependencies without Bun, Deno, npm, pnpm, or Yarn:
{
"config": {
"foxy": {
"manager": "native"
}
}
}Foxy then performs the installation itself, in PHP:
- It reads
dependencies,devDependencies, andoptionalDependenciesfrom the merged rootpackage.json; as in npm, anoptionalDependenciesentry overrides adependenciesentry of the same name. Eachfile:entry (the Composer assets and any other local package) is copied intonode_modules/<name>, and its owndependencies, non-optionalpeerDependencies, andoptionalDependenciesjoin the resolution with the same override rule. - It resolves every other dependency against the registry with asset-packagist semantics: a flat
node_moduleswith one version per package. Constraints from the root manifest, the local packages, and the transitivedependencies,peerDependencies, andoptionalDependenciesare intersected per package name, and the highest satisfying version wins. When a later constraint forces a package to a lower version, the constraints that its abandoned version had introduced are withdrawn, and optional dependencies never block a required one. Dist-tags such aslatestornextare honored. The resolver never backtracks to an older version of a package to escape a conflict that the dependencies of its highest acceptable version introduce; such a conflict, like one that only nestednode_modulescould solve, is reported as an error that names every constraint and its origin. Install such a project with a JavaScript manager instead. Unsatisfiable or missing optional dependencies are skipped with a warning, together with the whole subtree of an optional package whose own dependencies cannot be installed, and a deprecated selection is reported with the registry's deprecation text. Afile:package keeps the version its manifest declares; a constraint from another package that this version does not satisfy is reported as a conflict, unless thefile:entry is itself optional, in which case the local package is dropped like any other optional dependency. - It downloads each tarball from the URL published by the registry, verifies it against the registry's
integrityvalue (SHA-512, with the legacy SHA-1shasumas a fallback), and extracts it intonode_modules/<name>, stripping the archive's top-level directory like npm does. Tarballs are cached in<cache-files-dir>/foxy/and are subject to Composer'scache-files-ttl,cache-files-maxsize, andcache-read-onlysettings; every cached tarball is verified again before use. - It writes
foxy.lock, which records the root requirements and the requirements of every local package (regular and optional ones apart), and the selected version, tarball URL, and integrity of every installed package.composer installwith a lock that matches the current manifests installs exactly those tarballs without a single metadata request;composer update, or a lock that no longer matches, resolves again and rewrites the lock. Commitfoxy.lock. - It records what it installed in
.foxy-installed.jsoninside the install directory, the counterpart of npm'snode_modules/.package-lock.json: thefoxy.lockentry of every package left in place. A registry package whose directory exists and whose entry matches the lock is kept untouched, so a warm install extracts nothing; a package whose entry differs or whose directory is missing is extracted again.file:packages are always copied again, since their content can change without a version change. The file is removed at the start of each installation and written at its end, so an interrupted installation leaves none behind. A missing or malformed file only forces every registry package to be extracted again, as happens once on the first install that creates it; deleting it is always safe. Like every dot-entry, the pruning leaves it alone. Do not commit it. - It removes entries of the install directory that are not part of the installation (dot-entries such as
.binare left alone).
The install directory is node_modules next to package.json by default. native-install-dir changes it; a
relative value is resolved from root-package-json-dir, an absolute value is used as is. A Yii 2 application that
keeps the framework's default @npm alias can install straight into vendor/npm-asset:
{
"config": {
"foxy": {
"manager": "native",
"native-install-dir": "vendor/npm-asset"
}
}
}Foxy owns that directory: every entry that the lock does not list is removed on every install, except entries whose
name starts with a dot (such as .bin), so do not point it at a directory that also receives asset-packagist
packages from Composer. The root package directory, any of its parents, filesystem roots, and a directory that
overlaps a local file: package in either direction are rejected; symbolic links in the path are resolved before
that check as far as PHP's realpath() resolves them, which is incomplete on Windows, so treat the check as lexical
there. A symbolic link found inside the directory is removed as a link, never followed. Package
names read from package.json, foxy.lock, and the registry must follow npm's name grammar (an optional @scope/
followed by a name of letters, digits, ., _, and -, up to 214 characters); any other name is rejected before a
path is built from it. Scoped packages keep the npm layout (vendor/npm-asset/@popperjs/core), not asset-packagist's
popperjs--core form.
The native manager requires the PHP zlib extension. Its output follows Composer's verbosity levels: at normal
verbosity it prints only changes (Installing, Updating, and Removing lines), or
Nothing to install, update or remove; -v adds a Keeping line per kept package and a Refreshing line per
unchanged file: package copied again; -vv adds the registry traffic (metadata requests, tarball downloads,
prefetches, and cache hits); -vvv adds Composer's own HTTP log.
Its registry requests go through Composer's HTTP layer: proxies, cafile, disable-tls, and the credentials stored
in auth.json for the registry host all apply, so a private registry can be configured with registry-url plus
Composer authentication. Credentials are matched against the request's host:port exactly as written in
registry-url: localhost and 127.0.0.1 are not aliases of each other, and the auth.json key must include the
port when it is not the scheme's default (npm.example.com:4873). A plain http:// registry also needs Composer's
config.secure-http set to false. A registry that answers 401 or 403 fails the request with Composer's error text,
as any other registry failure does:
{
"config": {
"foxy": {
"manager": "native",
"registry-url": "https://npm.example.com"
}
}
}The registry is expected to answer the npm metadata API (GET /<name> with
Accept: application/vnd.npm.install-v1+json) and to serve the tarball URLs it publishes. composer foxy:audit also
needs npm's bulk advisories endpoint (POST /-/npm/v1/security/advisories/bulk with a JSON body); the registry must
implement it, and a registry that does not fails the audit with status 2. Verdaccio implements it only with its
audit middleware enabled (middlewares: audit: enabled: true); its default configuration answers 404. HTTP 429
responses are retried up to three times, honoring Retry-After up to 60 seconds; other failures are reported
immediately. Tarballs missing from the cache are first downloaded in parallel through Composer's HTTP downloader
(COMPOSER_MAX_PARALLEL_HTTP sets the limit, 12 by default) and then verified and installed one by one; a tarball
whose parallel download fails is requested again on its own, with the retries above, and its error is reported then.
With cache-read-only or an unwritable cache, tarballs are downloaded one by one only.
The native manager implements the subset that frontend assets need. It does not support:
npm:aliases,git,github:,http(s),workspace:, andlink:specifications; each is rejected with an error that names the dependency.- Nested
node_modules: two packages that require incompatible versions of the same dependency cannot be installed together. - Lifecycle scripts (
preinstall,install,postinstall,prepare),binlinks,os,cpu, andenginesfilters,overrides, andresolutions. Scripts are never executed; packages that need a build step at install time must be installed with a JavaScript manager. - Prerelease identifiers that Composer's version parser does not understand, such as
5.0.0-next.3or1.0.0-canary.1: those versions are skipped, and a range or dist-tag that selects one is rejected. Prereleases namedalpha,beta,RC,dev, andpatchare matched with npm's rule: only a range that names a prerelease of the samemajor.minor.patchcan select it. - Symbolic and hard links inside tarballs (skipped) and
..or absolute entry paths (rejected).
Selection by availability is also disabled for this manager: without manager: native or a foxy.lock file, a
project without a JavaScript manager still reports No asset manager was found..
Set a timeout in seconds for the frontend manager process:
{
"config": {
"foxy": {
"manager-timeout": 420
}
}
}Disable manager binary probing and execution while continuing to update package.json:
{
"config": {
"foxy": {
"run-asset-manager": false
}
}
}In this manifest-only mode, Foxy does not probe manager binaries, run install or update commands, or remove existing
node_modules/@composer-asset/* installations during npm reconciliation. Generated Composer asset manifests and the
root package.json are still updated. An explicit composer foxy:audit remains available and validates and runs the
selected manager because it represents a direct user request.
Both fallbacks are enabled by default. They can be controlled independently:
{
"config": {
"foxy": {
"fallback-asset": true,
"fallback-composer": false
}
}
}fallback-assetrestores the projectpackage.jsonwhen package merging fails, or when manager execution throws or returns a non-zero result.fallback-composerrestores the captured lock data and installed Composer dependencies for any exception or non-zero manager result during asset solving.
Foxy restores package.json first and Composer state second, and attempts the Composer restoration even when the
package.json restoration fails. It rethrows the original solve error when restoration succeeds. If a restoration also
fails, Foxy reports the rollback failure and retains the original error as the previous exception. Disabled fallbacks
do not create snapshots.
Composer lock and vendor state are captured at Composer's pre-operations-exec event. This is not a fully atomic
transaction for composer require or composer remove: Composer may update the root composer.json before that event,
and Foxy does not retain its previous bytes. Inspect and, when necessary, revert composer.json after a failed command.
By default, Foxy writes local mock packages under <vendor-dir>/php-forge/composer-asset/. Override the location with:
{
"config": {
"foxy": {
"composer-asset-dir": "runtime/foxy-assets"
}
}
}Foxy recursively resets this directory during each solve. To prevent deletion outside Foxy-owned storage, it rejects:
- An empty path, filesystem root, project root, vendor root, or a parent directory that contains either protected root.
- A symbolic link used as the asset directory.
- An existing, non-empty custom directory without Foxy's
.foxy-managedownership marker.
A new or empty custom directory is accepted and receives the marker automatically. Before upgrading an existing custom directory, verify that it contains only generated Foxy data, remove its contents, and let Foxy recreate and mark it.
Foxy normally processes installed packages that require Foxy or declare extra.foxy=true. The root project can include
or exclude package names with glob patterns or regular expressions:
{
"config": {
"foxy": {
"enable-packages": {
"foo/*": true,
"foo/legacy-*": false,
"/^acme\\/theme-/": true
}
}
}
}When no package must be excluded, a list is sufficient:
{
"config": {
"foxy": {
"enable-packages": ["foo/*", "/^acme\\/theme-/"]
}
}
}Patterns are evaluated in declaration order; the first matching pattern determines the result.
In the root application, root-package-json-dir controls the package.json read/write path and manager working
directory:
{
"config": {
"foxy": {
"root-package-json-dir": "web"
}
}
}Relative root-project paths are resolved from the Composer project directory. Absolute paths and filesystem roots are supported.
In an installed Composer library, the same option is resolved relative to that library's installation directory and
identifies the directory containing its embedded package.json. The resolved manifest must remain inside the
library's Composer installation directory.