Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -1348,7 +1348,7 @@ Claude Code's file permission checks match only `Edit(path)` and `Read(path)` ru
>
> The same override key also carries `sandbox`, the sibling top-level settings subtree governing the sandbox commands run in (`sandbox.network.*`, `sandbox.filesystem.*`, `sandbox.credentials`, `sandbox.allowAppleEvents`, ...). It has no canonical permission category either — it constrains _how_ a permitted command runs rather than which commands are permitted — so it is a verbatim passthrough on the same terms, merged into the top level of `settings.json` and round-tripped back on import — except for the three paths naming an executable, covered by the Trust caveat below, which are dropped in both directions. The merge is recursive, unlike the flat `permissions` fields above: `sandbox` subtrees carry restriction lists (`network.deniedDomains`, `filesystem.denyRead`), so setting one flag under `network` must not drop the denials beside it. A sibling key at any depth survives; a list you author replaces the existing list rather than being appended to. **Scope caveat:** Claude Code honors a subset of `sandbox.*` only from user settings, managed settings and the `--settings` flag — `filesystem.disabled`, `network.strictAllowlist`, `network.tlsTerminate`, `credentials.allowPlaintextInject`, `credentials.awsPairs`, `credentials.sigv4` and `allowAppleEvents` — and ignores them in a repository's `.claude/settings.json` / `.claude/settings.local.json`. Rulesync therefore skips those keys when generating project scope (warning once per key) and emits them only under `--global`, so it never _writes_ a project-scope sandbox policy that does nothing. The same restriction applies **per entry** inside `credentials.files` and `credentials.envVars`: an entry with `"mode": "mask"` is ignored in a repository's settings file, so Rulesync drops just those entries at project scope (warning once per list) while keeping the `deny` entries in the same list, which every scope does honor. Values already in the file are left untouched — both ones you hand-wrote and ones an earlier Rulesync version generated — because the two cannot be told apart and clobbering your file would be worse; so an inert key committed before this behavior existed stays there until you remove it by hand. Import stays scope-agnostic. See the [sandboxing docs](https://code.claude.com/docs/en/sandboxing).
>
> **Any other key is a top-level `settings.json` key.** Everything in the `claudecode` block other than `permission`, `permissions`, `sandbox` and `hooks` is written straight to the top level of `.claude/settings.json` and round-trips back on import, so a setting Claude Code adds needs no Rulesync release to become authorable — `editorMode`, `emojiCompletionEnabled`, `workflowSizeGuideline`, `keybindingFlavor`, `env`, `model`, `alwaysThinkingEnabled` and anything after them all work the same way. The merge is recursive, like `sandbox`, so setting one key under `env` keeps the variables already in the file. `hooks` is excluded because the [hooks feature](#rulesynchooksjsonc) owns it, `permission`/`permissions`/`sandbox` because they have their own handling above, and `$schema` because it is an editor pointer rather than a Claude Code setting. **Scope caveat:** the [settings reference](https://code.claude.com/docs/en/settings-reference) documents a scope per key, and Rulesync skips a key the file it is writing cannot honor (warning once per key): keys scoped `User or managed` / `User, local, or managed` (`spellcheck`, `autoMode`, `vimInsertModeRemaps`, `pluginConfigs`, `sshConfigs`, `syncClaudeAiSkills`, ...) are skipped at project scope and emitted only under `--global`, while keys scoped `Managed` (`allowManagedHooksOnly`, `requiredMinimumVersion`, `strictKnownMarketplaces`, ...) or `Global config` (`diffTool`, `autoConnectIde`, `teammateDefaultModel`, ...) are skipped in both scopes, because neither file Rulesync writes is the file that reads them — set those by hand in the managed settings file or `~/.claude.json`. The alternate spellings `additionalMarketplaces` and `allowedMarketplaces` are resolved to their canonical keys (`extraKnownMarketplaces` and `strictKnownMarketplaces`) before that check, so an alias is treated exactly as the key it spells. **Trust caveat:** a permissions file is shareable — `rulesync fetch` copies `.rulesync/permissions.jsonc` out of another repository — so a file whose name promises restrictions must not be able to hand Claude Code a command to run. Rulesync therefore refuses the keys whose value _is_ an executed command, in both scopes and in both directions: `apiKeyHelper`, `awsAuthRefresh`, `awsCredentialExport`, `fileSuggestion`, `gcpAuthRefresh`, `otelHeadersHelper`, `policyHelper`, `processWrapper`, `statusLine` and `subagentStatusLine` are never written (warning once per key) and are silently dropped on import — author commands in [`.rulesync/hooks.jsonc`](#rulesynchooksjsonc), where a reviewer expects them, or set these by hand in `settings.json`. The same line applies inside `sandbox`, which has its own merge branch: `sandbox.ripgrep`, `sandbox.bwrapPath` and `sandbox.socatPath` each name an executable Claude Code runs, so they are refused in both scopes and dropped on import too. Keys that widen what Claude Code trusts rather than running something themselves are still written, but each logs a warning naming what it affects: `env`, `disableAllHooks`, `disableSkillShellExecution` set to `false` (which re-opens inline shell execution a user setting had turned off), `enableAllProjectMcpServers`, `enabledMcpjsonServers` and `allowedMcpServers`, `autoMode`, `skipAutoPermissionPrompt` and `skipDangerousModePermissionPrompt`, `enabledPlugins` and `extraKnownMarketplaces`, `agent` and `outputStyle` (which replace the prompt and tools every session starts with), `httpHookAllowedEnvVars` and `allowedHttpHookUrls` (which relax what an existing HTTP hook may send and where), `permissions.additionalDirectories`, a `permissions.defaultMode` of `bypassPermissions`, `acceptEdits` or `auto`, and the `sandbox` paths that loosen the sandbox rather than naming something to run — `enabled`, `autoAllowBashIfSandboxed`, `allowUnsandboxedCommands`, `excludedCommands`, `allowAppleEvents`, `enableWeakerNestedSandbox`, `enableWeakerNetworkIsolation`, `ignoreViolations`, `filesystem.allowRead`, `filesystem.allowWrite`, `network.allowedDomains`, `network.allowMachLookup`, `network.allowUnixSockets`, `network.allowAllUnixSockets`, `network.allowLocalBinding`, `network.httpProxyPort` and `network.socksProxyPort`. The `allow*` lists are in that set because Claude Code merges a list across settings scopes rather than replacing it, so a project file can only ever add to them; their `deny*` counterparts, which restrict, are not. The sandbox warnings fire only on the value that actually loosens the policy, so authoring the restrictive one (`allowUnsandboxedCommands: false`, an empty `excludedCommands`) stays quiet. They are also emitted after the scope filter, so a path the scope drops is reported as skipped rather than as written. Every one of these warnings exists for the same reason: a value that arrived with a fetched override should be visible rather than silent. (`env` is warned about rather than refused because it has too many ordinary uses to drop, even though a value such as `NODE_OPTIONS` or `PATH` does run code.) As with `sandbox`, the filter only applies to what Rulesync writes: a value already in the file is left untouched, and removing a key from the override block does not delete the value an earlier generate wrote — clear it in `settings.json` by hand.
> **Any other key is a top-level `settings.json` key.** Everything in the `claudecode` block other than `permission`, `permissions`, `sandbox` and `hooks` is written straight to the top level of `.claude/settings.json` and round-trips back on import, so a setting Claude Code adds needs no Rulesync release to become authorable — `editorMode`, `emojiCompletionEnabled`, `workflowSizeGuideline`, `keybindingFlavor`, `env`, `model`, `alwaysThinkingEnabled` and anything after them all work the same way. The merge is recursive, like `sandbox`, so setting one key under `env` keeps the variables already in the file. `hooks` is excluded because the [hooks feature](#rulesynchooksjsonc) owns it, `permission`/`permissions`/`sandbox` because they have their own handling above, and `$schema` because it is an editor pointer rather than a Claude Code setting. **Scope caveat:** the [settings reference](https://code.claude.com/docs/en/settings-reference) documents a scope per key, and Rulesync skips a key the file it is writing cannot honor (warning once per key): keys scoped `User or managed` / `User, local, or managed` (`spellcheck`, `autoMode`, `vimInsertModeRemaps`, `pluginConfigs`, `sshConfigs`, `syncClaudeAiSkills`, ...) are skipped at project scope and emitted only under `--global`, while keys scoped `Managed` (`allowManagedHooksOnly`, `requiredMinimumVersion`, `strictKnownMarketplaces`, ...) or `Global config` (`diffTool`, `autoConnectIde`, `teammateDefaultModel`, ...) are skipped in both scopes, because neither file Rulesync writes is the file that reads them — set those by hand in the managed settings file or `~/.claude.json`. The alternate spellings `additionalMarketplaces` and `allowedMarketplaces` are resolved to their canonical keys (`extraKnownMarketplaces` and `strictKnownMarketplaces`) before that check, so an alias is treated exactly as the key it spells. **Trust caveat:** a permissions file is shareable — `rulesync fetch` copies `.rulesync/permissions.jsonc` out of another repository — so a file whose name promises restrictions must not be able to hand Claude Code a command to run. Rulesync therefore refuses the keys whose value _is_ an executed command, in both scopes and in both directions: `apiKeyHelper`, `awsAuthRefresh`, `awsCredentialExport`, `fileSuggestion`, `gcpAuthRefresh`, `otelHeadersHelper`, `policyHelper`, `processWrapper`, `statusLine` and `subagentStatusLine` are never written (warning once per key) and are silently dropped on import — author commands in [`.rulesync/hooks.jsonc`](#rulesynchooksjsonc), where a reviewer expects them, or set these by hand in `settings.json`. The same line applies inside `sandbox`, which has its own merge branch: `sandbox.ripgrep`, `sandbox.bwrapPath` and `sandbox.socatPath` each name an executable Claude Code runs, so they are refused in both scopes and dropped on import too. The scope caveat reaches inside `sandbox` as well: `sandbox.filesystem.allowManagedReadPathsOnly` and `sandbox.network.allowManagedDomainsOnly` are scoped `Managed`, so they are skipped in both scopes for the same reason the `Managed` top-level keys are — set them in the managed settings file by hand. Keys that widen what Claude Code trusts rather than running something themselves are still written, but every one of them is named in a single summary warning per file — one line listing each setting and what it affects, rather than a run of near-identical lines: `env`, `disableAllHooks`, `disableSkillShellExecution` set to anything but `true` (which re-opens inline shell execution a user setting had turned off), `enableAllProjectMcpServers`, `enabledMcpjsonServers` and `allowedMcpServers`, `autoMode`, `skipAutoPermissionPrompt` and `skipDangerousModePermissionPrompt`, `enabledPlugins` and `extraKnownMarketplaces`, `agent` and `outputStyle` (which replace the prompt and tools every session starts with), `httpHookAllowedEnvVars` and `allowedHttpHookUrls` (which relax what an existing HTTP hook may send and where), `claudeMdExcludes` (which drops the CLAUDE.md files its patterns match), `crossSessionInbound` set to anything but `hold` or `refuse` (which lets messages from your other sessions reach Claude), `modelOverrides` (which decides the inference profile a call is routed to), `skipWebFetchPreflight` set to anything but `false` (which turns off the WebFetch domain safety check), `remoteControlAtStartup` set to anything but `false`, `prUrlTemplate` (which rewrites the PR links Claude Code renders), `companyAnnouncements`, `permissions.additionalDirectories`, a `permissions.defaultMode` of `bypassPermissions`, `acceptEdits` or `auto`, and the `sandbox` paths that loosen the sandbox rather than naming something to run — `enabled`, `autoAllowBashIfSandboxed`, `allowUnsandboxedCommands`, `excludedCommands`, `allowAppleEvents`, `enableWeakerNestedSandbox`, `enableWeakerNetworkIsolation`, `ignoreViolations`, `filesystem.allowRead`, `filesystem.allowWrite`, `network.allowedDomains`, `network.allowMachLookup`, `network.allowUnixSockets`, `network.allowAllUnixSockets`, `network.allowLocalBinding`, `network.httpProxyPort` and `network.socksProxyPort`. The `allow*` lists are in that set because Claude Code merges a list across settings scopes rather than replacing it, so a project file can only ever add to them; their `deny*` counterparts, which restrict, are not. These warnings fire only on the value that actually loosens the policy, so authoring the restrictive one (`allowUnsandboxedCommands: false`, an empty `excludedCommands`) stays quiet. Each condition names the value that stays quiet rather than the one that warns, so a value of the wrong type — a `skipWebFetchPreflight` of `1` rather than `true` — is reported rather than passed over. `allowUnsandboxedCommands` and `autoAllowBashIfSandboxed` both default to `true`, and an explicit `true` is reported even so: a project `.claude/settings.json` outranks the user file, so writing it there re-opens what a user's `false` closed. `remoteControlAtStartup` is the one key whose _scope_ depends on its value — Claude Code honors a `false` from a project file but ignores a `true`, so a `true` is skipped at project scope and emitted only under `--global`. They are also emitted after the scope filter, so a path the scope drops is reported as skipped rather than as written. Every one of these warnings exists for the same reason: a value that arrived with a fetched override should be visible rather than silent. (`env` is warned about rather than refused because it has too many ordinary uses to drop, even though a value such as `NODE_OPTIONS` or `PATH` does run code.) As with `sandbox`, the filter only applies to what Rulesync writes: a value already in the file is left untouched, and removing a key from the override block does not delete the value an earlier generate wrote — clear it in `settings.json` by hand.

For OpenCode, this generates the `permission` object in `opencode.json` / `opencode.jsonc` (project mode) or `.config/opencode/opencode.json` / `.config/opencode/opencode.jsonc` (global mode), preserving other existing OpenCode config fields. OpenCode's `webfetch`, `websearch`, `todowrite`, `question`, and `doom_loop` keys accept only a single action string, so Rulesync emits their canonical `{ "*": "allow" }` form as `"allow"`. If one of these categories contains pattern-specific rules, Rulesync collapses them to the most restrictive action (`deny` > `ask` > `allow`) and logs a warning because OpenCode cannot represent those patterns; a map without `*` includes an implicit `ask` fallback so a narrow allowlist never becomes blanket `allow`, while an empty map becomes `deny` instead of falling through to OpenCode's default allow behavior.

Expand Down
Loading
Loading