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
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ A dispatch template in `.chezmoiexternals/` reads the BOM and generates the appr
When adding a tool: add a catalog entry, add a BOM entry, verify the correct source type is used (GitHub releases, direct vendor download, or package manager — don't conflate them).
When a tool extracts a full directory tree rather than a single binary, it needs its own `.chezmoiexternals/` entry rather than going through the shared dispatch template.

## Command approval policy

`.chezmoidata/ai/command_policy/*.toml` — one file per command family (e.g. `git.toml`, `beads.toml`), each declaring `[ai.command_policy.families.<name>]` with `prefixes` (literal command heads, no globs/regex).
Rendered into each tool's native permission-rule syntax by `dot_claude/modify_settings.json.tmpl` (`Bash(<prefix>:*)`) and `dot_gemini/antigravity-cli/modify_settings.json.tmpl` (`command(<prefix>)`).
New family = new file under `command_policy/`.
An overlay extends or shrinks an existing family via sibling `add`/`remove` prefix lists in the same `[ai.command_policy.families.<name>]` table — chezmoi list values replace wholesale on collision, so never restate `prefixes` directly — or disables one entirely via `enabled = false` (defaults to `true`, so base files never need to declare it).

## Data and templates

`.chezmoidata/` is the capability catalog — environment-neutral tool metadata, catalog entries, and structural additive data (config fragment registrations, plugin arrays, keybinds).
Expand Down
2 changes: 2 additions & 0 deletions src/chezmoi/.chezmoidata/ai/command_policy/beads.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[ai.command_policy.families.beads]
prefixes = ["bd"]
30 changes: 30 additions & 0 deletions src/chezmoi/.chezmoidata/ai/command_policy/git.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[ai.command_policy.families.git-local]
# Everything here is a local, commit-or-object-level operation with no
# remote/network write and no irrecoverable data loss (reflog covers commit
# history even across rebase). Deliberately excluded:
# - push: the actual remote-write / code-leak risk this list is scoped
# around; always prompt.
# - reset, clean: can permanently destroy uncommitted/untracked work with
# no reflog entry to recover from, and prefix matching can't separate
# safe flags (--soft) from destructive ones (--hard, -fd) — always
# prompt for both, in full.
prefixes = [
"git add",
"git blame",
"git branch",
"git checkout",
"git cherry-pick",
"git commit",
"git diff",
"git fetch",
"git log",
"git merge",
"git pull",
"git rebase",
"git restore",
"git show",
"git stash",
"git status",
"git switch",
"git worktree list",
]
8 changes: 5 additions & 3 deletions src/chezmoi/.chezmoidata/claude_code.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ failIfUnavailable = false
type = "command"
command = "statusline claude render"

[claude_code.settings.permissions]
tools = "allow"
files = "allow"
# permissions.allow is computed at render time from
# ai.command_policy.families (.chezmoidata/ai/command_policy.toml) —
# see dot_claude/modify_settings.json.tmpl. (Previously this table set
# tools/files = "allow" here, but neither key exists in Claude Code's
# documented permissions schema — removed as inert.)

[claude_code.settings.env]
DISABLE_AUTOUPDATER = "true"
Expand Down
21 changes: 20 additions & 1 deletion src/chezmoi/dot_claude/modify_settings.json.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,26 @@ try:
except Exception:
existing = {}

managed = json.loads("""{{ .claude_code.settings | toPrettyJson }}""")
# One-time cleanup: tools/files under permissions were never a real Claude
# Code settings key (silent no-op); deep_merge below only adds/updates keys,
# so drop them explicitly or they'd linger in the live file forever.
existing.get("permissions", {}).pop("tools", None)
existing.get("permissions", {}).pop("files", None)

{{- $allow := list -}}
{{- range $name, $family := dig "ai" "command_policy" "families" (dict) . -}}
{{- if dig "enabled" true $family -}}
{{- $prefixes := concat (dig "prefixes" (list) $family) (dig "add" (list) $family) -}}
{{- $removed := dig "remove" (list) $family -}}
{{- range $prefixes -}}
{{- if not (has . $removed) -}}
{{- $allow = append $allow (printf "Bash(%s:*)" .) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $settings := mergeOverwrite (deepCopy .claude_code.settings) (dict "permissions" (dict "allow" ($allow | uniq | sortAlpha))) }}
managed = json.loads("""{{ $settings | toPrettyJson }}""")


def deep_merge(d1, d2):
Expand Down
23 changes: 23 additions & 0 deletions src/chezmoi/dot_gemini/antigravity-cli/modify_settings.json.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ except Exception:
{{ $agyMethod := dig "agy" "installation_method" "" . }}
{{- if ne $agyMethod "none" }}

{{- $agAllow := list -}}
{{- range $name, $family := dig "ai" "command_policy" "families" (dict) . -}}
{{- if dig "enabled" true $family -}}
{{- $prefixes := concat (dig "prefixes" (list) $family) (dig "add" (list) $family) -}}
{{- $removed := dig "remove" (list) $family -}}
{{- range $prefixes -}}
{{- if not (has . $removed) -}}
{{- $agAllow = append $agAllow (printf "command(%s)" (regexQuoteMeta .)) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}

# Managed settings from chezmoi
{{ $settings := dict
"telemetry" (dict "enabled" .gemini.telemetry.enabled "logPrompts" .gemini.telemetry.log_prompts)
Expand All @@ -35,6 +48,9 @@ except Exception:
"selectedType" "oauth-personal"
)
)
"permissions" (dict
"allow" ($agAllow | uniq | sortAlpha)
)
"tools" (dict
"sandbox" .gemini.tools.sandbox
"sandboxAllowedPaths" .gemini.tools.sandbox_allowed_paths
Expand All @@ -59,6 +75,13 @@ except Exception:

managed = json.loads("""{{ $settings | toPrettyJson }}""")

# permissions.allow is union-merged rather than replaced: agy's own in-CLI
# "always allow" flow writes grants into this same file, and a wholesale
# replace here would wipe them on every `chezmoi apply`.
existing_allow = existing.get("permissions", {}).get("allow", [])
managed["permissions"]["allow"] = sorted(set(existing_allow) | set(managed["permissions"]["allow"]))


def deep_merge(d1, d2):
for k, v in d2.items():
if isinstance(v, dict) and k in d1 and isinstance(d1[k], dict):
Expand Down
Loading