Grok inspect loads main-checkout deny rules from a linked worktree

John Burns

grok inspect --json in the main checkout left the permission source list empty. The same command in a linked worktree named that checkout’s .claude/settings.local.json and reported loaded as 2. The worktree did not contain the file. The main checkout was projectTrusted: false. The worktree was projectTrusted: true.

Those two lines are not a view of one trust bit. The file the main inspect skipped is the file the worktree inspect counted. If you set GROK_WORKTREE_INHERITS_MAIN_APPROVALS=false and then read only the flag, you will miss the source that stayed.

What the two directories held

The binary was Grok Build 1.0.67, build 2fe064390d07, on Linux x86_64. The run was 2 November 2026. GROK_HOME pointed at a new directory. The normal grok home was not opened. No grok login was run. No prompt was sent. The command was grok inspect --json.

The main checkout was a new Git repository with one commit and a marker file. It was not a copy of a tree anyone uses. Two untracked files sat in it before the first paired inspect:

{
  "permissions": {
    "defaultMode": "dontAsk",
    "allow": ["Bash(echo main-allow)"],
    "deny": ["Bash(echo main-deny)"],
    "ask": ["Bash(echo main-ask)"]
  }
}

That object was .claude/settings.local.json. Beside it, .grok/config.toml held a separate allow string, a separate deny string, and [features] worktree_inherits_main_approvals = true. Neither file was committed.

The worktree was created with git worktree add, not with grok worktree create and not with grok -w. Its .git file pointed at the main checkout’s worktree metadata. git rev-parse --git-common-dir from the worktree printed the main checkout’s .git. The worktree directory itself held the marker file and that .git pointer. It had no .claude directory and no .grok directory.

A later control was git clone of that same commit into a third directory. The clone received the committed marker only. The settings file was untracked, so the clone did not get a copy.

The permissions chapter that grok inspect wrote into the disposable home, docs/user-guide/22-permissions-and-safety.md, says a worktree applies the deny and ask rules of the main checkout’s .claude/settings.local.json, under the same folder-trust gate as its own project rules. It says the allow rules and defaultMode in that file do not apply in worktrees. The sessions chapter says the same deny and ask crossing, and it separates that file from saved “always allow” answers. The config reference names features.worktree_inherits_main_approvals, default true, as the switch for the main checkout’s saved “always allow” answers. Never-allow answers apply either way. The Grok Build changelog entry for 1.0.67, read 2 November 2026, describes that saved-answer switch. It does not mention .claude/settings.local.json.

The count is deny plus ask

The first worktree inspect, with one allow, one deny, and one ask in the main file, listed one source and loaded 2. The source string ended in settings.local.json (settings) and the path was the main checkout, not the worktree. skipped was empty.

The file was then rewritten and the worktree inspect was repeated. Allow alone left sources empty and loaded at 0. Deny alone listed the main file and set loaded to 1. Ask alone did the same. Two deny strings plus one ask plus one allow listed the same file and set loaded to 3. defaultMode alone, with no rule arrays, left sources empty and loaded at 0.

A committed-style .claude/settings.json in the main checkout, holding one deny, one ask, and one allow, and with settings.local.json moved aside, produced an empty source list and loaded 0 from the worktree. Putting the local file back, still with one deny, one ask, and one allow, while that settings.json remained, returned loaded 2. The extra file did not add a count.

So the number on this binary is the deny lines plus the ask lines in the main checkout’s settings.local.json. An allow line does not increment it. defaultMode does not increment it. A settings.json beside the local file does not become a source. The shipped chapter says allow and defaultMode do not apply in the worktree. The count matches that sentence. It does not, by itself, prove a later tool call will deny echo main-deny. This run did not start a session. It shows what inspect reports as loaded before a session starts. The same chapter says permission rules are read when a session starts. Inspect is the check you can run without one.

The main checkout’s .grok/config.toml did not appear in the bare worktree’s configSources.layers. That array was empty. A deny written only in the native project file was not the source the worktree listed. The crossing source in this run was the Claude-compatible local file, which the permissions chapter says Grok reads because a worktree may not have its own ignored copy.

The inherit flag does not clear that source

GROK_WORKTREE_INHERITS_MAIN_APPROVALS=false was set for one worktree inspect and left unset for the process environment otherwise. The report still listed the main settings.local.json. loaded was still 2. projectTrusted was still true.

That is the miss if you treat the 1.0.67 changelog sentence as a worktree isolation switch. The sentence is about saved “always allow” answers from the main checkout. The shipped permissions chapter puts those answers in $GROK_HOME/sessions/, in a permission.toml scoped to the repository root, and it says the flag keeps the always-allow rows out of worktrees while never-allow rows still apply. This run did not write a permission.toml. It does not say whether a saved always-allow answer would have crossed, or whether the flag would have stopped that answer. It says the flag did not remove the settings.local.json source inspect was already listing.

The chapter also says a blanket approval of every shell command in the main checkout’s file does not carry over, and an edit setting other than rejecting edits does not carry over. The measured allow line and the measured defaultMode are the part of that file inspect did not count. The deny and ask lines are the part it did count, with the flag false.

If the question is whether a worktree session will honor a remembered always-allow from the main checkout, inspect’s loaded field is the wrong field. If the question is whether a personal deny in .claude/settings.local.json is visible to a linked worktree, the flag is the wrong switch. Read the source path. Then read the count.

Trust follows the directory that has a project file

The main inspect, after the local file was restored to one allow, one deny, and one ask, still said projectTrusted: false, sources empty, and loaded 0. Its configSources.layers named the main .grok/config.toml and nothing else. A subdirectory of that checkout kept the same projectRoot, stayed projectTrusted: false, and stayed at loaded 0. The untrusted bit on the main checkout did not stop the worktree from naming the file. It stopped the main inspect from counting the file.

The bare worktree had no project file of its own. Inspect marked it trusted and counted the main deny and ask lines. No trusted_folders.toml was written under the disposable home. true here is not a grant you saved with --trust or /hooks-trust. The short grok --help on this binary does not list --trust. The hooks chapter still says a headless run can pass --trust, and that a grant is recorded in trusted_folders.toml. This inspect never created that file.

Adding a .grok/config.toml to the worktree, with one deny string and no other keys, flipped the worktree report. projectTrusted became false. sources became empty. loaded became 0. configSources.layers named the worktree’s own config file, not the main checkout’s. Removing that worktree file put the previous report back: projectTrusted true, the main settings.local.json as the only source, loaded 2.

The gate inspect applied was the worktree’s, not the main checkout’s. A linked worktree with nothing for the trust check to hold is where the main personal deny and ask lines showed up. The moment the worktree itself looked like a project config, inspect held those lines back and did not say it had inherited them.

The separate clone makes the link specific. Before it had a settings file, inspect marked it trusted, left sources empty, and did not name the main checkout’s settings.local.json. After a local settings file was written into the clone, with its own allow, deny, and ask strings, inspect marked the clone untrusted and left loaded at 0. It still did not name the original checkout. A second directory that is not a linked worktree did not pick up the original path. A linked worktree with no copy of the file did.

That clone result also matches the main checkout: a directory that contains settings.local.json was untrusted on first inspect, and the rules in the file were not in loaded. The worktree result does not match that pattern, because the file it counted was not in the directory inspect marked trusted.

Verify the source path and the count

Run grok inspect --json in the main checkout and in the linked worktree, with the same GROK_HOME if you are testing a disposable home. Compare four fields.

projectTrusted on the main checkout can be false while the worktree is true. Do not read the main bit as the worktree bit.

permissions.sources in the worktree should be empty if you expected the worktree to ignore the main personal file. On this binary a linked worktree with no project file of its own listed the main .claude/settings.local.json. A source line that names the other directory is the inheritance. A source line that names the worktree is a file in the worktree.

loaded should be checked against the arrays, not against the file’s existence. Deny lines plus ask lines matched the count here. Allow lines and defaultMode did not. An empty source list with loaded 0 means inspect did not count the local file. It does not mean the file is absent on disk.

configSources.layers is a different list. The bare worktree left it empty even while permissions.sources named the Claude local file. A deny that exists only in .grok/config.toml was not the crossing source in this run. Do not clear one list and assume the other moved.

Repeat the worktree inspect with GROK_WORKTREE_INHERITS_MAIN_APPROVALS=false if you set that variable to isolate the worktree. If the settings.local.json source remains, the flag did not cover that file. The changelog sentence for 1.0.67 is still about saved always-allow answers. Those answers live in the session store the permissions chapter describes, which this inspect did not print.

If the worktree has its own .grok/config.toml and projectTrusted is false, a missing inherited source means inspect held the main local file back at the worktree’s trust gate. That is not the same as the file being absent, and it is not the same as the main checkout being the directory whose trust bit you just read.

This comparison used git worktree add. It does not say what grok worktree create or grok -w writes into the new directory before the first inspect. If that command copies or creates a project file, the trust bit can flip before you look, and the source list can be empty for that reason. Read the worktree directory before you explain an empty list.

The result is a source path and a count. The main checkout’s inspect did not show the personal deny. The linked worktree’s inspect did, until the worktree itself became an untrusted project. The flag named in the 1.0.67 note did not remove that source.

Sources

  • Grok Build 1.0.67 grok inspect --json, build 2fe064390d07, Linux x86_64, 2 November 2026. Disposable GROK_HOME. No login and no prompt.
  • Permissions, sessions, hooks, and config chapters written into that home by grok inspect on the same binary.
  • Grok Build changelog, 1.0.67 entry, read 2 November 2026.