A Grok sandbox exit 0 does not mean Landlock applied

John Burns

grok --sandbox devbox inspect --json exited 0. Stderr was empty. The JSON it printed had no sandbox field. The same run appended a row to sessions/sandbox-events.jsonl under the grok home:

event_type: ApplyFailed
profile: devbox
platform: linux/landlock
enforced: false
error: Landlock not available. Requires Linux kernel 5.13+ with Landlock enabled.

/sys/kernel/security/landlock was not on this host. /sys/kernel/security was. uname -r was 7.0.14-6-pve. The event sentence is not a version check. A kernel newer than 5.13 can still leave that sysfs node absent. /proc/config.gz and the matching /boot/config-* file were not there, so this note does not say which kernel option is missing. It says the node Grok’s own error names as the requirement was not present, and the command that asked for the profile still returned success.

That is the result worth keeping. An exit code from inspect is not a statement that the profile held.

Where the warning actually went

The sandbox page, last updated 21 July 2026, says Linux enforcement is Landlock, that the sandbox is off by default, and that a profile which cannot be applied logs a warning and continues. The user guide shipped inside Grok Build 1.0.67 says the same thing for a built-in profile, and it says sandbox events are written under the grok home’s sessions directory. It also says a custom profile is the exception: if that profile cannot be applied, Grok refuses to start rather than run with its protections missing.

On the continue path, the warning was the event row. It was not a line on the terminal. Four devbox runs wrote four ApplyFailed rows. Each inspect or worktree create that used the built-in name exited 0. grok doctor --json --sandbox workspace also exited 0, with empty stderr, both before bwrap was on PATH and after bubblewrap 0.12.0 was placed there. The doctor report did not mention Landlock, bubblewrap, or a sandbox profile. grok --sandbox workspace --version printed grok 1.0.67 (2fe064390d07) and exited 0. grok --sandbox workspace --help exited 0. A check that only runs version, help, or doctor will not see the gate that inspect, models, and worktree create hit.

grok --sandbox off worktree create exited 0 and did not write an ApplyFailed row. Off is the profile that does not claim enforcement. Devbox is the profile that records a failed apply and still returns success. If you read the exit code and skip the event file, those two outcomes look the same.

The inspect document is the wrong place to recover the miss. A search of the devbox JSON for the word sandbox returned nothing. The permissions block, the trust bit, and the config source list were present. The profile that had just failed to apply was not.

The three profiles that do not continue

workspace, read-only, and strict did not take that path. With bwrap absent from PATH, each of these exited 1 before inspect printed JSON and before worktree create printed a directory:

error: this sandbox could not enforce its deny list on Linux: bwrap exec failed: No such file or directory (os error 2). Install bubblewrap with `apt install -y bubblewrap`. Refusing to start with denied paths unprotected.

No deny list had been written. sandbox.toml in the disposable home was empty for those first runs. The guide reserves the hard refusal for a custom profile whose deny list needs bubblewrap, and it says the built-in devbox profile falls back to Landlock. Devbox did not call bwrap, and it did not exit 1. It also did not apply Landlock. The event row is the only place that distinction is written down.

The same install sentence was the whole stderr for three other selections. GROK_SANDBOX=read-only grok worktree create exited 1 with it. grok --sandbox workspace models exited 1 with it. grok inspect, with no sandbox flag, exited 1 with it when the only selection was this block in the grok home config:

[sandbox]
profile = "read-only"

The flag is not required. The config key is enough to make inspect itself refuse. That matters if a managed file or a copied config sets the profile and the next command you trust is grok inspect.

The flag and the environment variable are not one step when both are set. The guide lists an explicit --sandbox flag or GROK_SANDBOX as the first resolution step for a new session. On this binary, --sandbox off with GROK_SANDBOX=workspace still listed worktrees and exited 0. --sandbox workspace with GROK_SANDBOX=off exited 1 with the install sentence. The flag wins. A wrapper that exports the variable and also passes the flag will get the flag.

Outside a git repository, grok --sandbox read-only worktree create printed that same install sentence and exited 1. The worktree page says the “not a git repository” error comes first, and the profile refusal follows. With bubblewrap missing, the git error never appears. You cannot use that sentence to tell a non-repository apart from a sandbox refusal until bwrap is actually on PATH.

An unknown name failed as two errors, not one. grok --sandbox not-a-profile inspect --json exited 1. The first error said the custom profile was not found and printed a sample [profiles.not-a-profile] block. The path in that sample was the literal ~/.grok/sandbox.toml, even though GROK_HOME pointed at another directory. The second error was the same bubblewrap deny-list refusal. A missing name does not stop at “not found.” Later, a profile that did exist under $GROK_HOME/sandbox.toml was found, so the lookup follows GROK_HOME. The not-found text does not.

Putting the binary on PATH changes the line, not the result

Debian’s candidate package was bubblewrap 0.12.0-1~deb13u1. It was not installed with apt. The .deb was unpacked in the lab, and its bwrap was placed on PATH for the retest. bwrap --version printed bubblewrap 0.12.0. apt-cache policy bubblewrap still showed the package not installed. The host package state did not change.

read-only and strict still exited 1. Stderr was no longer the grok install sentence. It was one line from bwrap, and it did not name the profile:

bwrap: Can't mkdir parents for SOCKET-PATH: Permission denied

SOCKET-PATH here is a host socket path under /run. It was not an entry in a deny list. The event file did not gain a row for either profile.

A wrapper on PATH recorded the arguments and then ran that same binary. The plan was a re-exec. It bind-mounted / onto /, bind-mounted the path down to the grok home so that home stayed writable, then read-only-mounted an empty sentinel file over the grok home files the guide says these profiles write-deny: hooks, hooks-paths, config.toml, trusted_folders.toml, managed_config.toml, requirements.toml, sandbox.toml, bundled, and bundles. After those, it read-only-mounted the same sentinel over host socket paths under /run, then executed the grok binary with the original arguments.

The read-only and strict plans included the socket path that bwrap could not create parents for. The workspace plan masked a shorter set of /run sockets and did not include that path. So the mkdir line is not a generic bwrap failure. It is the first destination in that profile’s plan whose parent bwrap could not create. Installing the package, which is what the earlier error tells you to do, gets you as far as that plan. It does not apply the profile.

workspace, with the same bwrap on PATH, exited 1 with a different sentence:

error: could not apply the 'workspace' sandbox profile; see the warning above for the cause. Refusing to start with its protections missing.

There was no warning above it. Stdout was empty. RUST_LOG=debug did not add a line. The event file still had only the four devbox rows. The unified log under the disposable home had no Landlock line and no bubblewrap line. The inner process, after bwrap had started it, printed that sentence and nothing else. The sentence tells you to read a warning this failure does not print.

A custom profile does not inherit the devbox continue path by extending it. This block was in the disposable home’s sandbox.toml:

[profiles.lab-dev]
extends = "devbox"

grok --sandbox lab-dev inspect --json exited 1 with the same missing-warning sentence, and the argument wrapper was not invoked. Built-in devbox had continued after Landlock failed. The name that extends it did not. A second custom profile, extends = "devbox" with deny = ["/no/such/path"], did call bwrap. bwrap then failed with Can't mkdir parents for /no/such/path: Permission denied. A relative deny of a file that existed in the workspace, secret.pem, still produced the missing-warning sentence rather than a JSON document. The guide is right that a deny list which cannot be bound should refuse. The text you get is not the guide’s text, and an empty deny list on a custom profile is already enough to refuse on this host.

The built-in names were not shadowed. A [profiles.devbox] block that extended workspace and named a deny path did not change --sandbox devbox. That command still exited 0 and still did not call bwrap. A [profiles.workspace] block that extended devbox did not change --sandbox workspace. That command still refused. You cannot rename your way out of the workspace refusal by defining the built-in name in sandbox.toml.

The refusals did not leave worktrees behind. After the failed creates, grok --sandbox off worktree list showed two entries. Both were from the commands that had exited 0: one under off, one under devbox. The list printed $GROK_HOME in the path column rather than the expanded directory. The create command, on success, printed the expanded path. If you archive the create line, that is the line that contains the real directory.

What to read after the exit code

Read the event file before you treat a successful inspect as an enforced sandbox. On this host the continue path writes enforced: false there and leaves stderr empty. The hard-fail path for workspace, once bwrap is present, writes a pointer to a warning and does not write the event row. You need both places. For workspace you still do not get a cause.

Read the stderr shape before you follow the remedy. The install sentence appears when bwrap is not on PATH, including for a built-in profile with no deny list. The mkdir sentence appears when bwrap is on PATH and the plan contains a destination it cannot prepare. The missing-warning sentence appears when the process got far enough to refuse and did not print the reason. Those are three different stops. None of them is the warning-and-continue behavior the page describes for a built-in profile, except devbox, and devbox continues without enforcement.

grok doctor is not a substitute for either file. It exited 0 under --sandbox workspace in both PATH states. The profile gate and the doctor gate are not the same command.

Verification

Confirm the version before comparing a refusal with this note. grok --version printed grok 1.0.67 (2fe064390d07). A newer build can change the continue path or print the warning this one omits. This note is that binary.

Confirm the Landlock node before you trust a profile that claims to use it:

ls /sys/kernel/security/landlock
uname -r

On this host the node was absent and the kernel string was 7.0.14-6-pve. If the node is present, the devbox continue path measured here is not yours to assume. The event sentence that says 5.13 or later is not the check.

Use a disposable GROK_HOME. Do not point these commands at a home that holds credentials. The normal grok home was not the directory under test. Its sandbox.toml and config.toml were not modified.

command -v bwrap
grok --sandbox devbox inspect --json
grok --sandbox workspace inspect --json
grok --sandbox read-only inspect --json

Expect devbox to exit 0 with empty stderr if Landlock is missing the way it was here, and expect a new ApplyFailed row with enforced false in sessions/sandbox-events.jsonl. Expect workspace and read-only to exit 1. If bwrap is missing, expect the deny-list install sentence even with an empty sandbox.toml. If bwrap is present, expect read-only to print a bwrap mkdir line, and workspace to print the missing-warning sentence. Then open the event file and see whether the failing profile added a row. On this host it did not.

grok doctor --json can exit 0 on the same binary, including with --sandbox workspace. That is not a sandbox pass.

--sandbox off is the selection that still created a worktree here. devbox also created one, and the event row for that create still said the profile was not enforced. Remove a worktree you do not want with grok worktree rm. The two created here were under the disposable home.

Stop there. Exit 0 from inspect meant the process continued. For devbox on this host, the event file says Landlock did not apply. For workspace, the process stopped, and the warning it tells you to read was not on the terminal.

Sources

  • Grok Build 1.0.67, build 2fe064390d07, Linux x86_64, kernel 7.0.14-6-pve, 7 November 2026. The commands were grok inspect, grok worktree, grok doctor, grok models, grok --version, and grok --help, with GROK_HOME pointed at a new directory. No grok login was run. No prompt was sent. The normal grok home was not modified.
  • Bundled user guide 18-sandbox.md, written into that disposable home on first inspect. It says a built-in profile that cannot be applied warns and continues, says a custom profile refuses when it cannot be applied, says devbox falls back to Landlock, and says sandbox events are logged under the grok home’s sessions directory.
  • Sandbox page, last updated 21 July 2026: Sandbox. It names Landlock on Linux, lists the built-in profiles, and says a sandbox that cannot be applied logs a warning and continues. It does not mention bubblewrap.
  • Worktrees page, last updated 21 July 2026: Worktrees. It says a worktree command outside a git repository reports that fact first.
  • bubblewrap 0.12.0 from Debian package bubblewrap_0.12.0-1~deb13u1_amd64.deb, sha256 70aca4fa8daeacb677ec00e8063eb586f08ae3d94b1f11e684370b5524c43431, unpacked in the lab and placed on PATH. It was not installed with apt. apt-cache policy bubblewrap still showed the package not installed.