CageFS hides almost everything in /usr/bin, /usr/sbin, and /opt from end users by
default — and that's the point. The cage is a per-user virtual filesystem with a curated
copy of system binaries, libraries, and config. Anything not in the skeleton doesn't exist
as far as the user is concerned, even if it's installed on the host.
The flip side is that every site builder eventually hits a missing tool. wp is on the
host but the user gets command not found over SSH. Composer works as root but cron jobs
fail. A custom Bash script the reseller wants their clients to call returns nothing. This
guide walks through the right way to expose a binary inside the cage — via a CageFS
configuration file, not by hand-symlinking inside the skeleton.
How the skeleton works
CageFS builds a single template directory (/usr/share/cagefs-skeleton by default) by
reading every .cfg file under /etc/cagefs/conf.d/. Each user's cage is a bind-mounted
view of that skeleton, plus their own home directory. Anything you want visible to users
has to be declared in a config file and the skeleton has to be rebuilt — manually copying
a binary into the skeleton works once and then gets wiped on the next cagefsctl --force-update.
The two directives that matter:
paths=— file paths to expose inside the cage. The file is copied (or symlinked, depending on type) into the skeleton.comments=— free-text description shown incagefsctl --list-applied-configs.
That's almost the whole API. The complexity is in knowing which paths a given binary actually needs.
Step 1 — Find every file the binary depends on
A binary is rarely self-contained. Composer needs PHP, WP-CLI needs PHP plus a wrapper script, a Node CLI needs Node plus a shebang and the script. Before writing the config, trace what the binary actually opens:
ldd $(which composer)
For a PHP-based tool, ldd returns "not a dynamic executable" because the file is a Phar
script. Read the shebang instead:
head -n 1 /usr/local/bin/composer
If the shebang points to /usr/bin/php, that interpreter has to be in the cage too —
which it already is on a CloudLinux cPanel server, since alt-php is a standard skeleton
entry. If your tool's shebang points to a custom Node or Python path under /opt, you'll
need to expose the interpreter in the same config file.
For native binaries, follow the ldd output. Every .so listed has to either already be
in the skeleton or be explicitly included.
Step 2 — Write a CageFS config file
Create one file per logical tool under /etc/cagefs/conf.d/. The filename is arbitrary
but the convention is <toolname>.cfg:
cat > /etc/cagefs/conf.d/composer.cfg <<'EOF'
[composer]
comments=Composer dependency manager for PHP
paths=/usr/local/bin/composer
EOF
If the binary lives somewhere unusual, list every path it needs explicitly:
cat > /etc/cagefs/conf.d/custom-tools.cfg <<'EOF'
[custom-tools]
comments=In-house helper scripts and dependencies
paths=/usr/local/bin/site-deploy, /usr/local/bin/site-rollback, /opt/in-house/lib/, /opt/in-house/bin/
EOF
A few rules the parser cares about:
- The section name in square brackets has to match the filename stem (
composer.cfg→[composer]). Mismatches are silently ignored. - Multiple paths go on one line, comma-separated. Trailing commas break the parse.
- A path ending in
/is treated as a directory and recursed. Without the slash it's a single file.
Step 3 — Rebuild the skeleton
The config file doesn't take effect until the skeleton is regenerated:
cagefsctl --force-update
This takes 30 seconds to 2 minutes depending on disk speed and skeleton size. It does not affect logged-in user sessions — those keep their existing bind mount until the next session.
If you only changed one config file and the full rebuild is too slow, update just that one:
cagefsctl --addrpm composer
cagefsctl --force-update-etc
--addrpm here is a misnomer — it works for any .cfg file, not just RPM-backed ones.
Step 4 — Verify the binary is visible
Test as a real user, not as root. Root bypasses the cage:
su - someuser -c "which wp; wp --version"
If which returns the path but the command fails with a library or interpreter error,
the binary made it into the cage but one of its dependencies didn't. Re-run ldd and
add the missing libraries to the config.
If which returns nothing, the skeleton wasn't rebuilt — re-run cagefsctl --force-update.
Common cases
WP-CLI
WP-CLI is a Phar that ships under /usr/local/bin/wp with a #!/usr/bin/env php shebang.
Since alt-php is already exposed, the only path to add is the wrapper itself:
[wp-cli]
comments=WordPress CLI
paths=/usr/local/bin/wp
If your users SSH in and wp runs the system PHP instead of their selected alt-php
version, that's a PATH issue, not a cage issue. See setting up the PHP
Selector for how the per-user PHP wrapping works —
the same wrapping makes wp pick up the user's selected version automatically when
called via the Selector-aware php.
Node CLI tools (Yarn, pnpm, npx)
If you're using CloudLinux's Node.js
Selector, the per-user Node binaries live under
/opt/alt/alt-nodejs*/root/usr/bin/ and are exposed automatically. System-wide tools like
a globally installed yarn at /usr/bin/yarn need to be added explicitly:
[yarn]
comments=Yarn package manager
paths=/usr/bin/yarn, /usr/lib/node_modules/yarn/
The node_modules directory is the critical part — without it, yarn runs but immediately
errors on missing internal modules.
Git
Modern CloudLinux skeletons already include /usr/bin/git, but git lfs, git-crypt,
and other extension binaries are not. Expose them as separate paths:
[git-extras]
comments=Git LFS and extensions
paths=/usr/bin/git-lfs, /usr/libexec/git-core/git-lfs
Removing an exposed binary
Delete the config file and rebuild:
rm /etc/cagefs/conf.d/custom-tools.cfg
cagefsctl --force-update
The skeleton is fully rebuilt from the surviving .cfg files, so removed paths disappear.
You can also exclude a specific user from CageFS entirely with cagefsctl --disable <user>, but that's a sledgehammer — it turns off all isolation for that account, not just
the missing binary.
Don't edit the skeleton directly
It's tempting to cp a binary straight into /usr/share/cagefs-skeleton/usr/local/bin/
and call it done. Two reasons not to:
- The next
cagefsctl --force-update(triggered by any RPM transaction or CloudLinux update) wipes manual additions. - CageFS hashes the skeleton against the declared configs; out-of-band files cause
integrity warnings in
cagefsctl --checkruns.
If you find yourself reaching for a manual copy, write the .cfg file instead — it's the
same two lines and survives upgrades.
Verifying the full config set
To see every config currently applied:
cagefsctl --list-applied-configs
And to validate that the live skeleton matches what the configs declare:
cagefsctl --check
A clean run prints nothing. Any output is a discrepancy worth investigating before the next CloudLinux update.
For broader CageFS troubleshooting, see fixing 'Cannot manage PHP versions' when CageFS is disabled — many "binary missing" symptoms turn out to be CageFS being off for the user, not a skeleton gap.
FAQ
Why is my custom binary missing inside CageFS?+
Do I need to restart anything after adding a CageFS config?+
Will my CageFS config survive a CloudLinux update?+
How do I add a binary that's a shell script?+
Can I expose a binary to one user but not all users?+
Why does WP-CLI find PHP but fail to run inside CageFS?+
Next steps
- For the underlying PHP setup that CageFS protects, see setting up CloudLinux PHP Selector on cPanel.
- If users are hitting CageFS errors rather than just missing binaries, work through fixing 'Cannot manage PHP versions' when CageFS is disabled.
- Activating CloudLinux for a new server? Start with the CloudLinux licence and the install guide for cPanel.