diff --git a/.claude/settings.json b/.claude/settings.json index 85d70d670b..d2f5622593 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,28 +6,88 @@ }, "permissions": { "allow": [ - "Read", "Edit", - "Write", + "Read", + "WebFetch(domain:*)", "WebSearch", - "WebFetch(domain:*)" + "Write" ], "deny": [ + "Bash(gh api * --method DELETE *)", + "Bash(gh api * --method PUT *)", + "Bash(gh api * -X DELETE *)", + "Bash(gh api * -X PUT *)", + "Bash(gh api * /actions/secrets*:*)", + "Bash(gh api repos/*/actions/variables*:*)", + "Bash(gh api repos/*/collaborators*:*)", + "Bash(gh api repos/*/keys*:*)", + "Bash(gh api repos/*/pulls/*/merge:*)", + "Bash(gh extension *)", + "Bash(gh gpg-key *)", + "Bash(gh issue transfer*)", + "Bash(gh pr merge*)", + "Bash(gh repo delete*)", + "Bash(gh secret *)", + "Bash(gh ssh-key *)", + "Bash(gh variable *)", + "Bash(gh workflow disable*)", + "Bash(gh workflow enable*)", + "Bash(git branch -D*)", + "Bash(git clean*)", "Bash(git push --force*)", "Bash(git reset --hard*)", - "Bash(git clean*)", - "Bash(git branch -D*)", - "Bash(rm -rf*)", - "Bash(sudo*)", - "Bash(gh workflow enable*)", - "Bash(gh workflow disable*)" + "Bash(sudo*)" ], "ask": [ - "Bash(git commit*)", - "Bash(git push*)", "Bash(docker run*)", - "Bash(gh api*)", - "Bash(gh workflow run*)" + "Bash(gh * archive*)", + "Bash(gh * cancel*)", + "Bash(gh * close*)", + "Bash(gh * comment*)", + "Bash(gh * create*)", + "Bash(gh * delete*)", + "Bash(gh * develop*)", + "Bash(gh * edit*)", + "Bash(gh * field-*)", + "Bash(gh * fork*)", + "Bash(gh * item-*)", + "Bash(gh * link*)", + "Bash(gh * lock*)", + "Bash(gh * mark-template*)", + "Bash(gh * pin*)", + "Bash(gh * ready*)", + "Bash(gh * rename*)", + "Bash(gh * reopen*)", + "Bash(gh * rerun*)", + "Bash(gh * review*)", + "Bash(gh * unlink*)", + "Bash(gh * unlock*)", + "Bash(gh * unmark-template*)", + "Bash(gh * unpin*)", + "Bash(gh * upload*)", + "Bash(gh api * --method *)", + "Bash(gh api * -X *)", + "Bash(gh api enterprises/*:*)", + "Bash(gh api orgs/*:*)", + "Bash(gh api repos/*/actions/permissions*:*)", + "Bash(gh api repos/*/actions/workflows/*/dispatches:*)", + "Bash(gh api repos/*/branches/*/protection*:*)", + "Bash(gh api repos/*/environments*:*)", + "Bash(gh api repos/*/hooks*:*)", + "Bash(gh api repos/*/releases*:*)", + "Bash(gh api repos/*/rulesets*:*)", + "Bash(gh api teams/*:*)", + "Bash(gh auth *)", + "Bash(gh codespace *)", + "Bash(gh config set*)", + "Bash(gh label clone*)", + "Bash(gh pr checkout*)", + "Bash(gh project copy*)", + "Bash(gh repo set-default*)", + "Bash(gh repo sync*)", + "Bash(gh workflow run*)", + "Bash(git commit*)", + "Bash(git push*)" ] }, "sandbox": { @@ -67,17 +127,17 @@ "allowWrite": [ ".", "/tmp", - "~/.cache/pre-commit", + "~/.ansible", "~/.cache/pip", - "~/.venvs", - "~/.ansible" + "~/.cache/pre-commit", + "~/.venvs" ], "denyRead": [ - "~/.ssh", + "~/.aws", + "~/.config/gcloud", "~/.gnupg", "~/.kube", - "~/.aws", - "~/.config/gcloud" + "~/.ssh" ] } } diff --git a/docs/contributing/tools/agents/claude/settings.md b/docs/contributing/tools/agents/claude/settings.md index f1b0da504f..82fc373261 100644 --- a/docs/contributing/tools/agents/claude/settings.md +++ b/docs/contributing/tools/agents/claude/settings.md @@ -30,7 +30,7 @@ For Bash specifically, `sandbox.autoAllowBashIfSandboxed: true` adds an implicit ## Allow Permissions ✅ -The allowlist is intentionally short. With sandboxed Bash auto-allowed, only non-Bash tools and the wildcarded web tools need explicit entries. +The allowlist covers non-Bash tools and the wildcarded web tools. Read-only `gh api` calls (default GET, no `-X`/`--method`) are not listed explicitly: they match no `ask` or `deny` rule and therefore execute via sandbox auto-allow without prompting. | Permission | When | Why | Security | |---|---|---|---| @@ -63,8 +63,28 @@ These operations pause and require explicit operator approval before executing, | `git commit*` | Creating a permanent history entry. | The operator MUST review the staged diff and message before committing. | Commits are persistent and visible to all contributors after push. | | `git push*` | Publishing changes to the remote. | Cannot be undone without a force-push. | Exposes changes to all repository collaborators and CI. | | `docker run*` | Starting a standalone container outside the compose stack. | Each invocation carries a unique risk profile depending on flags. | Can mount host paths, expose ports, and run privileged containers. | -| `gh api*` | Making direct GitHub REST or GraphQL API calls. | Can modify branch protection, secrets, webhooks, collaborators, and workflow triggers. | Supports arbitrary HTTP methods (`-X POST/PATCH/DELETE`). Each invocation MUST be reviewed individually. | -| `gh workflow run*` | Triggering a CI workflow run on the remote. | Consumes runner minutes, executes with workflow secrets, and produces remotely-visible results. Equivalent in effect to `git push` for triggered runs. | Each invocation MUST be reviewed to confirm the target workflow and inputs. | +| `gh api * -X *` / `gh api * --method *` | Any explicit non-GET HTTP verb against the GitHub API. | The default `gh api` verb is GET (read-only). Specifying `-X` or `--method` always indicates a write — POST/PATCH/DELETE/PUT — so the prompt forces a per-call review. PUT and DELETE are blocked outright by `deny`; POST and PATCH land here. | Captures every mutating call regardless of endpoint. The DELETE/PUT-specific deny rules below take precedence and reject those verbs unconditionally. | +| `gh api enterprises/*:*` | Any enterprise-scoped API call. | Enterprise endpoints administer org membership, billing, and policy across multiple orgs. | Reviewer MUST confirm the enterprise slug and intended scope. | +| `gh api orgs/*:*` | Any organization-scoped API call. | Org endpoints govern membership, teams, and org-wide settings. | Reviewer MUST confirm the org and the specific endpoint. | +| `gh api repos/*/actions/permissions*:*` | Reading or mutating Actions permissions on a repo. | Toggles whether forks may run workflows, restricts the action allowlist, and gates repo-level CI policy. | Reviewer MUST confirm the policy delta before approval. | +| `gh api repos/*/actions/workflows/*/dispatches:*` | Triggering a workflow via `workflow_dispatch`. | Equivalent in effect to `gh workflow run` — runs CI with workflow secrets and produces remotely-visible results. | Reviewer MUST confirm the workflow ref and inputs payload. | +| `gh api repos/*/branches/*/protection*:*` | Reading or mutating branch protection on a specific branch. | Branch protection gates pushes, merges, and required checks. PUT is already blocked unconditionally; this pattern catches GET-audit and explicit-PATCH paths. | Reviewer MUST confirm the rule delta. | +| `gh api repos/*/environments*:*` | Reading or mutating Actions environments. | Environments gate deployment secrets and required reviewers; weakening them exposes secrets to additional workflows. | Reviewer MUST confirm the environment and gating change. | +| `gh api repos/*/hooks*:*` | Reading or mutating repository webhooks. | Hooks deliver events to external systems; misconfiguration can leak data or break integrations. | Each call MUST be reviewed for target URL and event scope. | +| `gh api repos/*/releases*:*` | Reading or mutating GitHub releases. | Release creation and asset uploads are user-visible publishing actions. | Reviewer MUST confirm the tag and asset payload before approval. | +| `gh api repos/*/rulesets*:*` | Reading or mutating branch/tag rulesets (branch protection successor). | Rulesets gate merges and pushes; weakening them affects every contributor. | Each call MUST be reviewed for the rule scope and required-checks impact. | +| `gh api teams/*:*` | Any team-scoped API call. | Team endpoints govern membership and permission grants. | Reviewer MUST confirm the team and intended change. | +| `gh * *` (archive, cancel, close, comment, create, delete, develop, edit, fork, link, lock, pin, ready, rename, reopen, rerun, review, unlink, unlock, unpin, upload) | Any `gh` CLI invocation whose second token is a known mutating verb. | The wildcard in the namespace slot collapses 21 mutating verbs across `gh issue/pr/release/repo/ruleset/run/cache/gist/label/project/…` into a single rule per verb. Future namespaces that ship the same verb (e.g. a hypothetical `gh discussion lock`) are caught automatically — explicit per-namespace lists rot as the `gh` CLI grows. | Read-only subcommands (`view`, `list`, `status`, `diff`, `checks`, `watch`, `download`, `clone`) do not match and remain auto-allowed via the sandbox, mirroring the GET-vs-mutation split used for `gh api`. The `deny` rules above take precedence for the highest-blast verbs (e.g. `gh repo delete`, `gh secret *`, `gh ssh-key *`). | +| `gh * field-*` / `gh * item-*` / `gh * mark-template*` / `gh * unmark-template*` | Compound subcommands (e.g. `gh project item-create`, `gh project field-delete`, `gh project mark-template`). | The single-token `gh * *` patterns above only match when the third token starts with the verb. Compound names like `item-create` or `mark-template` slip through because the third token is the full compound, not the bare verb. These rules close that gap for Projects v2 mutations namespace-wide. | Catches all current and future compound-verb mutations without requiring per-verb enumeration. | +| `gh auth *` | Any `gh auth` subcommand (login, logout, refresh, setup-git, status, switch, token). | Whole namespace asked: `logout` is operator-scope DoS; `token` prints the OAuth/PAT to stdout where it would land in the transcript permanently if approved (operator MUST decline unless they explicitly want the token captured); `switch` silently changes the active identity; `setup-git` rewrites global git config; `login`/`refresh`/`status` are lower-risk but kept inside the same gate for symmetry. The trade-off vs. an explicit `deny` on `logout`/`token` is that approval is now a single click — operators must read the prompt before tapping through. | Reviewer MUST confirm intent for every invocation; in particular `gh auth token` MUST be declined unless the operator deliberately wants the token in the transcript. | +| `gh codespace *` | Any codespace subcommand (create, ssh, cp, delete, ports, …). | Codespaces are billable, persistent VMs with full repo + secret access; `ssh`/`cp` open a remote shell or move files in/out. The whole namespace is asked because even read commands (`list`, `view`) interact with billable infrastructure. | Each invocation MUST be reviewed individually for billing + data-flow impact. | +| `gh config set*` | Sets a `gh` CLI config key. | `gh config set editor /tmp/evil.sh` plants a binary that runs the next time any `gh` subcommand opens an editor (e.g. `gh issue create` without `--body`) — a deferred-execution backdoor that survives the current session. Persistent CLI config belongs to a deliberate operator decision. | Reviewer MUST confirm the key and value, especially for `editor`, `pager`, `git_protocol`. | +| `gh label clone*` | Copies all labels from one repository into another. | Mutation against the destination repo. Verb `clone` is not in the wildcard list because it is otherwise read-only-ish (e.g. `gh repo clone` is just a git clone). | Reviewer MUST confirm both source and destination repos. | +| `gh pr checkout*` | Switches the local working tree to a PR branch. | Does not check for uncommitted changes by default — silently overwrites them. The risk is to local state, not remote, but the loss is unrecoverable. | Reviewer MUST confirm the working tree is clean (or stashed) before approving. | +| `gh project copy*` | Duplicates an entire Projects v2 board. | Verb `copy` is not in the wildcard list. The duplicate is created under the operator's identity and may be public-by-default depending on org settings. | Reviewer MUST confirm the source/destination owners and visibility. | +| `gh repo set-default*` | Sets the default repository for `gh` invocations in the current directory. | Mutates per-directory `gh` config; subsequent ambiguous `gh pr/issue/run` calls would silently target the new default, which can mis-route mutations. Verb `set-default` is not in the wildcard list. | Reviewer MUST confirm the new default repository. | +| `gh repo sync*` | Syncs a fork against its upstream. | Pulls upstream commits into the fork (the operator's repo). Verb `sync` is not in the wildcard list because `gh repo clone`-style reads use related verbs that should remain frictionless. | Reviewer MUST confirm the source/destination repos and that the local working tree tolerates the sync. | +| `gh workflow run*` | Triggering a CI workflow run on the remote. | Consumes runner minutes, executes with workflow secrets, and produces remotely-visible results. Equivalent in effect to `git push` for triggered runs. The verb is `run`, which is not in the wildcarded mutating-verb list above, so it needs its own entry. | Each invocation MUST be reviewed to confirm the target workflow and inputs. | ## Deny Rules 🚫 @@ -72,14 +92,29 @@ These operations are unconditionally blocked, regardless of any `allow` entry or | Permission | Reason | |---|---| +| `gh api * -X DELETE *` / `gh api * --method DELETE *` | DELETE is irreversible from the API surface — deleting a release, branch ref, deploy key, or webhook destroys data with no undo. Both `-X` and `--method` spellings are blocked so neither shorthand slips through. | +| `gh api * -X PUT *` / `gh api * --method PUT *` | PUT replaces a resource wholesale. On endpoints like branch protection or rulesets, a PUT silently overwrites the entire policy with whatever payload is sent — partial JSON drops every unspecified field. PATCH (the merge variant) still goes through `ask`. | +| `gh api * /actions/secrets*:*` | Touches GitHub Actions secrets — reading metadata, creating, updating, or deleting. Even GETs leak secret names; mutations rotate credentials CI depends on. Out-of-band secret management only. | +| `gh api repos/*/actions/variables*:*` | Touches Actions variables. Variables are unencrypted secrets-lite — workflows read them like secrets, and they are equally load-bearing for CI. Out-of-band variable management only. | +| `gh api repos/*/collaborators*:*` | Reads or mutates repository collaborator membership. Mutations escalate access; even GETs leak the contributor list. Access changes belong to a human reviewer. | +| `gh api repos/*/keys*:*` | Reads or mutates repository deploy keys. Deploy keys are full-power repo-auth credentials — adding one creates a persistent backdoor; removing one breaks deployments. Manage out-of-band only. | +| `gh api repos/*/pulls/*/merge:*` | Merges a pull request via API, bypassing the `gh pr merge` confirmation flow. Merges are destructive to branch state and trigger downstream CI/deploys. | +| `gh extension *` | Any `gh extension` subcommand. `install` and `upgrade` pull remote code that then runs in-process under the operator's GitHub credentials on every subsequent `gh` invocation; `remove`/`list`/`search`/`exec` are blocked alongside to keep the namespace simple. Extensions belong to a deliberate operator decision, not an agent. | +| `gh gpg-key *` | Any `gh gpg-key` subcommand. `add` installs a persistent commit-signing identity an attacker could use to impersonate the operator on signed commits; `delete` can lock the operator out of signed-commit policies; `list` leaks the key-fingerprint inventory. Out-of-band key management only. | +| `gh issue transfer*` | Moves an issue (with its history and attachments) into a different repository. The destination repo can be attacker-controlled — equivalent to data exfiltration of the issue's contents. Issue transfers must be a deliberate human action. | +| `gh pr merge*` | CLI parallel to the `/pulls/*/merge` API deny. Merges are destructive to branch state and trigger downstream CI/deploys. | +| `gh repo delete*` | Permanently deletes a repository. No undo from the API; recovery requires GitHub support and is not guaranteed. | +| `gh secret *` | Any `gh secret` subcommand. `set`/`remove` mutate CI secrets under the operator's identity; `list` leaks secret names — exact same risk class as the `gh api * /actions/secrets*` deny ("Even GETs leak secret names"). Out-of-band secret management only. | +| `gh ssh-key *` | Any `gh ssh-key` subcommand. `add` is a persistent auth backdoor giving shell-equivalent push access until revoked; `delete` can lock the operator out; `list` leaks the key-fingerprint inventory. Out-of-band key management only. | +| `gh variable *` | Any `gh variable` subcommand. Mirror of the `gh secret *` rule for Actions variables. Variables are unencrypted secrets-lite; even `list` exposes their values directly, so the entire namespace is blocked and managed out-of-band. | +| `gh workflow disable*` | Disabling a workflow can silently turn off security-critical CI (CodeQL, security-review, lint gates). Must never be done autonomously. | +| `gh workflow enable*` | Re-enabling a workflow can silently restart paused CI (e.g. security scans deliberately disabled during incident response) without operator review. | +| `git branch -D*` | Force-deletes local branches regardless of merge status. Can destroy unmerged work with no recovery path. | +| `git clean*` | Any invocation of `git clean` is blocked. All useful variants (`-f`, `-fd`, `-xfd`) force-delete untracked files with no recovery path; the wildcard covers every flag order. Can silently wipe in-progress work the agent has not yet surfaced to the operator. | | `git push --force*` | Rewrites remote history. Can permanently destroy other contributors' work. | | `git reset --hard*` | Discards all uncommitted local changes without any recovery path. | -| `git clean*` | Any invocation of `git clean` is blocked. All useful variants (`-f`, `-fd`, `-xfd`) force-delete untracked files with no recovery path; the wildcard covers every flag order. Can silently wipe in-progress work the agent has not yet surfaced to the operator. | -| `git branch -D*` | Force-deletes local branches regardless of merge status. Can destroy unmerged work with no recovery path. | | `rm -rf*` | Recursive force-delete with no confirmation and no undo. Sandbox `allowWrite` does not protect here because `.` (the repo) is writable; `rm -rf .` would erase the working tree and `.git/`. | | `sudo*` | Prevents privilege escalation attempts. The sandbox already blocks privileged operations, but denying `sudo` outright avoids accidental approval prompts and makes the intent explicit. | -| `gh workflow enable*` | Re-enabling a workflow can silently restart paused CI (e.g. security scans deliberately disabled during incident response) without operator review. | -| `gh workflow disable*` | Disabling a workflow can silently turn off security-critical CI (CodeQL, security-review, lint gates). Must never be done autonomously. | ## Environment Overrides 🌳 diff --git a/tests/lint/repository/test_claude_settings_sorted.py b/tests/lint/repository/test_claude_settings_sorted.py new file mode 100644 index 0000000000..e4785c53ca --- /dev/null +++ b/tests/lint/repository/test_claude_settings_sorted.py @@ -0,0 +1,42 @@ +import json +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[3] +SETTINGS_PATH = REPO_ROOT / ".claude" / "settings.json" + +SORTED_ARRAYS: list[tuple[str, ...]] = [ + ("permissions", "allow"), + ("permissions", "deny"), + ("permissions", "ask"), + ("sandbox", "network", "allowedDomains"), + ("sandbox", "filesystem", "allowWrite"), + ("sandbox", "filesystem", "denyRead"), +] + + +class TestClaudeSettingsSorted(unittest.TestCase): + """Lint .claude/settings.json: every list-of-strings array we curate by hand + must stay ASCII-sorted so diffs are reviewable and merge conflicts minimal.""" + + @classmethod + def setUpClass(cls) -> None: + with open(SETTINGS_PATH) as fh: + cls.settings = json.load(fh) + + def test_arrays_are_ascending(self) -> None: + for path in SORTED_ARRAYS: + label = ".".join(path) + with self.subTest(array=label): + obj = self.settings + for key in path: + obj = obj[key] + self.assertEqual( + obj, + sorted(obj), + f"{label} is not ASCII-sorted ascending in {SETTINGS_PATH}", + ) + + +if __name__ == "__main__": + unittest.main()