'Allo :-)
I've been trying to (possibly fruitlessly given the recent headlines) been trying to limit the blast radius of our LLM coding agents. While trying to get claude to behave ok inside a sandbox some oddities came up which I hadn't hit before. I've checked what it said it found and the symptoms and findings seem legit. I didn't want to do an out of the blue PR as I can see v4 is in the works. But this might feed into that.
And thank you all for your work on Lando - it's been a huge help to me and my team. Really - your collective work has been much appreciated :-) ❤️
Claude says (yes, I know) after being run in a sandbox :
Summary
On macOS, lando discovers the Docker Desktop version by shelling out to defaults read on
Docker.app's Info.plist (getMacProp in lib/daemon.js:42). In sandboxed environments the
defaults tool fails even when the plist file itself is readable, because defaults does not
simply read the file (it goes through the macOS preferences machinery). getMacProp swallows
that failure into null (.catch(() => null)), and the compatibility scan then calls
semver.clean(null) (lib/engine.js:222), which throws:
TypeError: Cannot read properties of null (reading 'trim')
at Object.clean (/snapshot/core/node_modules/semver/functions/clean.js:5:27)
at /snapshot/core/utils/is-compatible-version.js:11:53
at /snapshot/core/lib/engine.js:212:63
...
at /snapshot/core/lib/engine.js:222:8
The result is that every engine-touching command (lando artisan, lando composer,
lando mysql, ...) dies at bootstrap with an error that gives no hint the problem is the
Desktop version probe. lando version still works, since it never reaches the engine scan.
Environment
- lando v3.26.2 (also checked
lib/daemon.js on main - the probe is unchanged)
- macOS (Darwin 25.6.0, Apple Silicon)
- Docker Desktop 4.85.0
- Sandbox: nono 0.72.0, which wraps commands in a macOS
Seatbelt profile. Agent-sandboxing tools like this are increasingly common (Claude Code and
friends), which is how the crash was found: an AI coding agent running lando artisan
inside a sandbox gets the TypeError above with no clue what went wrong.
Reproduction
Inside a Seatbelt-sandboxed shell that grants read access to
/Applications/Docker.app/Contents/Info.plist and full access to the Docker socket
(docker version completes a full client/server round-trip):
$ defaults read /Applications/Docker.app/Contents/Info.plist CFBundleShortVersionString
2026-08-10 19:55:00.147 defaults[56080] The domain/default pair of
(/Applications/Docker.app/Contents/Info.plist, CFBundleShortVersionString) does not exist
$ plutil -extract CFBundleShortVersionString raw /Applications/Docker.app/Contents/Info.plist
4.85.0
$ lando artisan --version
TypeError: Cannot read properties of null (reading 'trim') # stack as above
The same defaults read command succeeds on the host outside the sandbox. So the file is
present and readable; it is specifically the defaults tool that cannot operate in this
context (observed behaviour; the mechanism appears to be that defaults works through the
preferences daemon rather than direct file reads, and that path is blocked under Seatbelt -
note its misleading "does not exist" error for what is really an access problem).
Suggested fix
Two small, independent changes:
-
Probe the plist with plutil instead of (or as a fallback to) defaults in
getMacProp (lib/daemon.js:42):
const getMacProp = prop => shell.sh(['plutil', '-extract', prop, 'raw',
`${MACOS_BASE}/Contents/Info.plist`])
.then(data => _.trim(data))
.catch(() => null);
plutil ships with every macOS release lando supports and reads the file directly, so it
works in sandboxed contexts (verified above).
-
Degrade gracefully when a version probe returns null. daemon.getVersions() already
has a 'skip' escape hatch on the Windows path (lib/daemon.js:258); the darwin path could
do the same when the probe fails, so the compatibility scan reports "could not determine
Docker Desktop version" instead of crashing with an unrelated-looking TypeError. As it
stands, semver.clean(null) at lib/engine.js:222 throws on any environment where the
probe fails, whatever the reason.
'Allo :-)
I've been trying to (possibly fruitlessly given the recent headlines) been trying to limit the blast radius of our LLM coding agents. While trying to get claude to behave ok inside a sandbox some oddities came up which I hadn't hit before. I've checked what it said it found and the symptoms and findings seem legit. I didn't want to do an out of the blue PR as I can see v4 is in the works. But this might feed into that.
And thank you all for your work on Lando - it's been a huge help to me and my team. Really - your collective work has been much appreciated :-) ❤️
Claude says (yes, I know) after being run in a sandbox :
Summary
On macOS, lando discovers the Docker Desktop version by shelling out to
defaults readonDocker.app's Info.plist (
getMacPropinlib/daemon.js:42). In sandboxed environments thedefaultstool fails even when the plist file itself is readable, becausedefaultsdoes notsimply read the file (it goes through the macOS preferences machinery).
getMacPropswallowsthat failure into
null(.catch(() => null)), and the compatibility scan then callssemver.clean(null)(lib/engine.js:222), which throws:The result is that every engine-touching command (
lando artisan,lando composer,lando mysql, ...) dies at bootstrap with an error that gives no hint the problem is theDesktop version probe.
lando versionstill works, since it never reaches the engine scan.Environment
lib/daemon.jsonmain- the probe is unchanged)Seatbelt profile. Agent-sandboxing tools like this are increasingly common (Claude Code and
friends), which is how the crash was found: an AI coding agent running
lando artisaninside a sandbox gets the TypeError above with no clue what went wrong.
Reproduction
Inside a Seatbelt-sandboxed shell that grants read access to
/Applications/Docker.app/Contents/Info.plistand full access to the Docker socket(
docker versioncompletes a full client/server round-trip):The same
defaults readcommand succeeds on the host outside the sandbox. So the file ispresent and readable; it is specifically the
defaultstool that cannot operate in thiscontext (observed behaviour; the mechanism appears to be that
defaultsworks through thepreferences daemon rather than direct file reads, and that path is blocked under Seatbelt -
note its misleading "does not exist" error for what is really an access problem).
Suggested fix
Two small, independent changes:
Probe the plist with
plutilinstead of (or as a fallback to)defaultsingetMacProp(lib/daemon.js:42):plutilships with every macOS release lando supports and reads the file directly, so itworks in sandboxed contexts (verified above).
Degrade gracefully when a version probe returns null.
daemon.getVersions()alreadyhas a
'skip'escape hatch on the Windows path (lib/daemon.js:258); the darwin path coulddo the same when the probe fails, so the compatibility scan reports "could not determine
Docker Desktop version" instead of crashing with an unrelated-looking TypeError. As it
stands,
semver.clean(null)atlib/engine.js:222throws on any environment where theprobe fails, whatever the reason.