Skip to content

Issues uncovered while running an LLM agent in a sandbox #492

Description

@ohnotnow

'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:

  1. 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).

  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions