register_argc_argv=Off Bit Me: Debugging php -r in a Hardened CI Image
A one-liner that works on every laptop and dies in CI. The cause is a php.ini flag you've probably never set on purpose: register_argc_argv. This post is part 1 of "The Ops Log" — four incidents from running a self-hosted GitLab CI + TYPO3/FrankenPHP platform solo, each with a lesson that outlives the incident.
TL;DR
TL;DR: Hardened container images ship register_argc_argv=Off. With it off, $argv is null inside php -r — so php -r '…$argv…' file silently loses its argument. Pass data via stdin instead; it doesn't depend on the ini.
What happened
I was folding eight lint jobs into one (that's part 2 of this series). One folded check validates JSON. When the repo's jsonlint binary isn't installed, I added a fallback that shells out to PHP's own parser, once per file:
check_json — first attempt
# find every json file, hand each to php -r
find . -name '*.json' -exec php -r '
foreach (array_slice($argv, 1) as $f) {
json_decode(file_get_contents($f));
if (json_last_error() !== JSON_ERROR_NONE) { exit(1); }
}' {} +
Green on my machine. In CI, the job went red with a message that made no sense for code I'd just tested:
job trace
PHP Fatal error: Uncaught TypeError: array_slice():
Argument #1 ($array) must be of type array, null given
in Command line code:2
$argv was null. Not empty — null. On a dev machine the CLI SAPI populates $argv by default, so you never notice. The golden CI image is built from a hardened base with register_argc_argv=Off in php.ini — a common "we don't need argv in production" hardening. With that flag off, $argv and $argc simply aren't there, and php -r can't see the filename you passed it.
You can reproduce the whole thing in three lines — no container required:
repro
$ php -r 'var_dump($argv);' hello.json
array(2) { [0]=> "Standard input code" [1]=> "hello.json" } ✓ works
$ php -d register_argc_argv=Off -r 'var_dump($argv);' hello.json
NULL ✗ null
The fix isn't to force the flag back on with -d register_argc_argv=1 (fragile — you're fighting the image). It's to stop depending on argv at all. Pipe the file list in NUL-delimited on stdin and read it with stream_get_contents(STDIN):
check_json — the fix
find . ! -path '*/vendor/*' -name '*.json' -print0 \
| php -r '$bad=0;
foreach (array_filter(explode("\x00", stream_get_contents(STDIN)), "strlen") as $f) {
json_decode(file_get_contents($f));
if (json_last_error() !== JSON_ERROR_NONE) {
fwrite(STDERR, "✗ invalid JSON: $f — ".json_last_error_msg()."\n"); $bad=1;
}
} exit($bad);'
Verified by simulating the exact CI condition locally — -d register_argc_argv=Off — so it can't regress the same way. Valid set exits 0; a malformed file exits 1 with a readable line. Bonus catch you can see above: the first attempt also scanned vendor/, where dependencies ship intentionally malformed JSON fixtures — excluding it was half the battle.
The lesson
php -rplus$argvis a portability landmine: it leans on a php.ini default that hardened images turn off. Anything meant to run in someone else's container should take its input from stdin, not argv.
Frequently asked questions
Does this only affect the JSON-lint fallback in this example?+
No — any php -r snippet that relies on $argv/$argc is affected in an image with register_argc_argv=Off. The JSON-lint fallback is just the concrete case where it became visible.
Is it enough to just turn register_argc_argv back on?+
Technically yes, with -d register_argc_argv=1, but that fights the hardened image instead of working with it — the setting can disappear again with any image update. The more robust fix is to take input from stdin instead of argv.
Why doesn't this show up on my own machine?+
Because the CLI SAPI populates $argv by default on dev machines — register_argc_argv=Off is typically only set in hardened production/CI images, not locally.
Conclusion
A single php.ini flag that almost nobody sets on purpose turned a working one-liner into a silent CI blocker. The lesson extends beyond this one case: anything meant to run in someone else's hardened container should take its input in a way that doesn't depend on defaults like this one.
I harden and debug your CI pipelines on hardened base images — from PHP quirks to GitLab runner configuration.
Auditing your CI jobs for silent dependencies on php.ini defaults, environment variables, and image assumptions, plus migration to more robust, image-independent patterns.
Platform operations, not consulting on paper: I build and harden your CI/CD pipelines on an ongoing basis.
About the author
![[Translate to English:] Foto von Kai Ole Hartwig.](/fileadmin/_processed_/e/9/csm_ole-neu_73323ad80d.jpeg)
Kai Ole Hartwig
Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.
