Skip to content

feat(ai): global command-approval allowlist for Claude Code and Antigravity#716

Merged
mkobit merged 3 commits into
mainfrom
feat/global-command-approval-policy
Jul 14, 2026
Merged

feat(ai): global command-approval allowlist for Claude Code and Antigravity#716
mkobit merged 3 commits into
mainfrom
feat/global-command-approval-policy

Conversation

@mkobit

@mkobit mkobit commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds .chezmoidata/ai/command_policy.toml: one tool-neutral catalog of named command "families" (git-local, beads), each a flat list of safe command prefixes.
  • Renders that catalog into each tool's native permission-rule syntax — Bash(<prefix>:*) for Claude Code, command(<prefix>) for Antigravity — via small additions to the existing modify_settings.json.tmpl scripts for each tool. Mirrors the existing guidelines.toml "one source, many renderers" pattern.
  • Overlay-extensible without restating base data: a family can be extended/shrunk via add/remove prefix lists, disabled via enabled = false, or a whole new sibling family can be added — all through normal chezmoi dict-merge, working around chezmoi's list-values-replace-on-collision behavior.
  • Antigravity's render union-merges with whatever's already in the live settings file, since agy's own in-CLI "always allow" flow writes grants into that same array — a wholesale replace would wipe those on every chezmoi apply. Claude's side fully owns the array (its own "don't ask again" persists into project-level settings.local.json, not the user-level file this manages).
  • Drops the tools/files keys from claude_code.toml's permissions block — verified against Claude Code's permissions reference that neither key exists in the documented schema; they were inert.

git-local covers status/log/diff/show/blame/worktree-list/fetch/pull/add/commit/branch/switch/checkout/restore/stash/rebase/merge/cherry-pick — all local, commit-or-object-level operations, reflog-recoverable. Deliberately excludes push (the actual remote-write/leak risk), and reset/clean (can permanently destroy uncommitted/untracked work with no reflog entry, and prefix-matching can't separate safe flags like --soft from destructive ones like --hard/-fd).

Test plan

  • Rendered both modify_settings.json.tmpl files via chezmoi execute-template and confirmed valid Python + expected JSON shape.
  • Ran the rendered Python scripts against the real live ~/.claude/settings.json and ~/.gemini/antigravity-cli/settings.json — confirmed correct merge output, tools/files stripped, Antigravity's existing manual grants preserved.
  • Verified the overlay add/remove/sibling-family composition mechanism with a temporary throw-away data file (removed after, no trace in this PR).
  • chezmoi diff against the live home directory shows only the intended permissions block changes — no other settings touched.
  • chezmoi apply not yet run for real on this machine — pending after merge.

mkobit added 3 commits July 14, 2026 09:27
…ravity

Defines safe-command families once in .chezmoidata/ai/command_policy.toml
(git-local, beads) and renders them into each tool's native permission
syntax, so approvals no longer have to be re-accumulated per project.
Overlays can extend/shrink a family via add/remove, or add sibling
families, without restating chezmoi's list-replace-on-collision data.

Also drops tools/files under claude_code.toml's permissions block —
neither exists in Claude Code's documented settings schema, verified
against the permissions reference; they were inert.
One file per family under .chezmoidata/ai/command_policy/ (git.toml,
beads.toml) instead of one combined file — matches the existing
.chezmoidata/bin/ per-tool catalog pattern, so adding a family later is
a new file, not an edit. Documents the schema/overlay mechanism in
AGENTS.md next to the analogous BOM section rather than repeating it
per data file.
Renderers already default enabled to true when absent (dig "enabled"
true $family), so declaring it in git.toml/beads.toml was a no-op.
Only an overlay disabling a family needs to write enabled = false.
@mkobit mkobit merged commit 6a8d27b into main Jul 14, 2026
6 checks passed
@mkobit mkobit deleted the feat/global-command-approval-policy branch July 14, 2026 22:28
mkobit added a commit that referenced this pull request Jul 15, 2026
…717)

* refactor(ai): flatten command_policy to a command->bool dict

Families + prefixes + add/remove/enabled all existed to work around
chezmoi replacing list values wholesale on collision. Storing entries
as dict keys instead removes the problem at the root: an overlay adds
a command by adding a key, or removes one by setting it false — plain
dict merge, same pattern .chezmoidata/ai/sandbox.toml already uses.

Renders byte-identical output to the schema this replaces (verified
against the live ~/.claude/settings.json and
~/.gemini/antigravity-cli/settings.json applied in PR #716) — pure
schema simplification, no behavior change.

* feat(ai): allow/ask/deny tri-state for command_policy commands

Replaces the true/false value with a string mode matching both tools'
real permission tiers (Claude Code has permissions.allow/.deny/.ask;
Antigravity's docs state "Deny > Ask > Allow" precedence). "ask" (or
an unset command) renders nowhere in either tool -- absence from
allow/deny is what falls back to a normal per-use prompt.

All current commands stay "allow" -- no behavior change, verified by
re-rendering against the live settings files with zero diff. deny
rendering is wired but unexercised (no command uses it yet); the
Antigravity deny(...) key name is inferred from documented precedence
language, not yet live-tested, flagged as a comment in the template.

* docs(ai): move command_policy detail to src/chezmoi/AGENTS.md

Root AGENTS.md keeps a 1-2 sentence summary + link; full schema/
rendering/overlay detail moves to src/chezmoi/AGENTS.md, next to the
Notable cross-cutting features bullet (matches the existing sandboxr
pattern). Not placed inside .chezmoidata/ai/command_policy/ itself --
tried that first and it breaks every template reading chezmoi data
(chezmoi parses every file under .chezmoidata/ as a data source; a
.md file there errors with "unknown format").

* docs(ai): use markdown links instead of backtick paths for cross-references

* fix(ai): antigravity permissions fully replaced, not union-merged

User preference: chezmoi should own permissions.allow/.deny outright
rather than preserving whatever agy's own "always allow" flow wrote
into the live file -- matches Claude's existing full-replace behavior.

Also fixes a real bug this surfaced: $agPermissions (built to carry
an optional "deny" key) was computed but never wired into the actual
$settings dict, which still built permissions inline with only
"allow" -- deny was dead code for this tool, unverified until now.
Confirmed working with a throwaway test entry (not committed).

* refactor(ai): convert both settings.json modify scripts to pure chezmoi:modify-template

Drops the embedded-Python round-trip entirely for both
dot_claude/modify_settings.json and
dot_gemini/antigravity-cli/modify_settings.json, matching the
existing dot_codex/modify_config.toml precedent (fromJson .chezmoi.stdin
-> template logic -> toPrettyJson). Requires dropping both the .tmpl
suffix and the executable bit -- chezmoi's chezmoi:modify-template
dispatch doesn't recognize the file otherwise (confirmed empirically;
the executable-script code path silently takes over instead and
.chezmoi.stdin never gets populated).

toPrettyJson's return value always carries a trailing "\n" that trim
markers can't remove (they only affect surrounding template text, not
a pipeline's runtime value) -- piped through trimSuffix "\n" to match
the no-trailing-newline convention both live files already use.

No behavior change: both verified via chezmoi diff against the live
settings files (zero diff), including the deny bucket (tested with a
throwaway, uncommitted data file, both tools).

* fix(claude): move editorMode to the documented settings.json location

editorMode is a documented top-level settings.json key
(code.claude.com/docs/en/settings); ~/.claude.json is documented as
OAuth session / MCP config / per-project trust state / caches, not a
settings store. The current claude_code.preferences ->
modify_dot_claude.json.tmpl injection (PR #718) targets the wrong
file, same category of bug as the tools/files no-op keys already
removed from this same settings block.

Confirmed settings.json is the real, effective location before
adding this: the live ~/.claude/settings.json already had
editorMode = "vim" (from toggling /config previously, not from any
chezmoi-managed source) -- Claude Code itself reads/writes it there.

Also fixed a stale comment referencing the old
ai.command_policy.families / single-file .chezmoidata/ai/command_policy.toml
shape, superseded by this branch's flat commands dict under
.chezmoidata/ai/command_policy/*.toml.

Not removing the ~/.claude.json injection yet -- that file is
modified on the separate open PR #718 branch; removing
claude_code.preferences here risks a merge-order-dependent break of
that script. Left as an explicit follow-up for after both PRs merge.

* fix(claude): stop managing ~/.claude.json entirely, delete the dead injection

editorMode was the only key claude_code.preferences ever injected into
~/.claude.json via modify_dot_claude.json.tmpl. Now that it lives in
claude_code.settings (-> ~/.claude/settings.json, the documented
location, previous commit), that injection is a pure no-op -- and
~/.claude.json itself is documented as OAuth session / MCP config /
trust state / caches, not a place a dotfiles repo should be
templating content into at all.

Deletes modify_dot_claude.json.tmpl and claude_code.preferences
outright rather than leaving inert infrastructure around. This also
makes moot a separate bug found while investigating: the file had
lost its executable bit (required for chezmoi's legacy external-
script modify_ mechanism) and chezmoi diff was rendering its raw
unexecuted Python source as the literal target content -- would have
replaced the live ~2000-line ~/.claude.json with ~46 lines of garbage
had it ever been applied in that state. Deleting the file removes the
bug's entire surface rather than patching a mechanism with no reason
to exist anymore.

Updates dot_claude/AGENTS.md (single managed file now, not two) and
the stale modify_dot_claude.json.tmpl cross-reference in
dot_claude/modify_settings.json.

Supersedes PR #718, which was fixing formatting on the now-deleted
file -- closing that PR instead of merging it.

* docs: default modify_ scripts to chezmoi:modify-template

Ported from PR #718 (being closed, superseded -- the file it fixed,
modify_dot_claude.json.tmpl, was deleted outright in the previous
commit rather than patched). Documents the two sharp edges found
empirically (no .tmpl suffix, no executable bit -- both break
chezmoi's dispatch silently) and the trimSuffix "\n" fix for
toPrettyJson's baked-in trailing newline, plus the general key-
ordering caveat for files another live process also writes to.

* docs: reformat AGENTS.md prose to one sentence per line
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant