mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
chore(claude): tighten gh permission rules and lint settings.json sort order
Consolidate gh ask/deny entries via namespace-wildcard patterns, cover compound subcommands (item-*, field-*, mark-template*), and add a lint test that enforces ASCII-sorted permission and sandbox arrays in .claude/settings.json. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
25f01ccfc4
commit
ee30f6d446
3 files changed
+163
-26
No files matched your search
+79
-19
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 * <verb>*` (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 * <verb>*` 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 🌳
|
||||
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user