Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
df79bfe
docs: revise hardening spec after prior-art research (OD-78)
andrzej-janczak Jun 11, 2026
3fbe1f5
docs: finalize plan-time decisions in hardening spec (OD-78)
andrzej-janczak Jun 11, 2026
eeaa45b
docs: implementation plan for hardening the Claude agent (OD-78)
andrzej-janczak Jun 11, 2026
eced7a1
docs: correct CODACY_API_TOKEN to account-scoped, drop unachievable r…
andrzej-janczak Jun 12, 2026
e7dfd2b
docs: backend-dev high-level design overview with mermaid diagrams (O…
andrzej-janczak Jun 12, 2026
af62233
docs: run claude on Haiku model in both pipelines (OD-78)
andrzej-janczak Jun 12, 2026
f01a3c8
docs: clarify e2e fixture must be a git checkout with a Codacy origin…
andrzej-janczak Jun 12, 2026
4116112
feat: run configure-codacy-cloud on Haiku model in both pipelines
andrzej-janczak Jun 12, 2026
f6ed946
docs: record successful Haiku end-to-end test run (OD-78)
andrzej-janczak Jun 12, 2026
40c0663
feat: allow app.dev/app.staging.codacy.org in egress + DNS allowlist …
andrzej-janczak Jun 12, 2026
708bd3d
test: add hardening verification harness scaffold (OD-78)
andrzej-janczak Jun 12, 2026
84c83a3
feat: privilege-separate into runner/agent users with sudo CLI shims …
andrzej-janczak Jun 12, 2026
a495fb4
feat: store Codacy credentials in runner home (700), move tool-cache …
andrzej-janczak Jun 12, 2026
805a74f
feat: entrypoint pre-auths Codacy as runner and drops to agent with s…
andrzej-janczak Jun 12, 2026
be76971
feat: add localhost Anthropic auth proxy holding the real key as runn…
andrzej-janczak Jun 12, 2026
8bbe7f3
feat: tighten Claude tool policy + managed-settings lock, run dontAsk…
andrzej-janczak Jun 12, 2026
5588401
feat: shared setgid /workspace/.codacy for runner<->agent config hand…
andrzej-janczak Jun 12, 2026
02c285b
feat: scrub git token from clone + sanitize summary before upload (OD…
andrzej-janczak Jun 12, 2026
2abe2e2
feat: DNS allowlist via local dnsmasq, sinkhole non-allowlisted looku…
andrzej-janczak Jun 12, 2026
41b5dd9
chore: drop unused Gemini path; require ANTHROPIC_API_KEY in entrypoi…
andrzej-janczak Jun 12, 2026
80057a7
test: add opt-in cli + e2e probes (real keys) (OD-78)
andrzej-janczak Jun 12, 2026
af4d75b
docs: document two-user security model and verification harness (OD-78)
andrzej-janczak Jun 12, 2026
bd5808b
fix: provide Codacy token to runner-side CLI via file, not codacy log…
andrzej-janczak Jun 12, 2026
236802e
fix: drop CLAUDE_CODE_SUBPROCESS_ENV_SCRUB (needs bubblewrap), broade…
andrzej-janczak Jun 12, 2026
177edc0
docs: hardening test results — keyless suite + live e2e on Haiku (OD-78)
andrzej-janczak Jun 12, 2026
73f28a2
chore: gitignore planning/scaffolding docs, keep hardening-overview.m…
andrzej-janczak Jun 15, 2026
ae4d65b
fix: PR review — validate CLI name, agent-writable /workspace, both-s…
andrzej-janczak Jun 15, 2026
8e8c0e5
fix: git safe.directory for /workspace so runner-run codacy can auto-…
andrzej-janczak Jun 15, 2026
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
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
CODACY_API_TOKEN=
ANTHROPIC_API_KEY=
GEMINI_API_KEY=

# Defaults to the current directory if not set
# SOURCE_PATH=/path/to/repo
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
.idea/
analysis-cli/
codacy-cloud-cli/
gin-autoconfig-test-go/
gin-autoconfig-test-go/
.DS_Store
docs/superpowers/
docs/test-results-hardening-2026-06-12.md
docs/test-run-haiku-2026-06-12.md
74 changes: 74 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What This Is

A Docker image that runs an AI-powered Codacy configuration skill. Claude Code (the `claude` CLI) is the runtime; the container provides a least-privilege sandbox with the right tools and an outbound firewall. The actual configuration logic lives in the **`configure-codacy-cloud` skill**, pulled from [`codacy/codacy-skills`](https://github.com/codacy/codacy-skills) at image build time and baked into `/home/agent/.claude/commands/`.

The container **does not run local static analysis**. It tunes a repository's Codacy Cloud configuration via Cloud reanalysis only.

## Build and Run

```bash
# Build image
docker compose build

# Run against the current directory (set SOURCE_PATH to point elsewhere)
docker compose run --rm codacy-ai
```

Required env vars in `.env` (copy from `.env.example`): `CODACY_API_TOKEN` + `ANTHROPIC_API_KEY`. `CODACY_API_TOKEN` is a Codacy **Account API Token** (account-scoped — there is no repo-scoped token that can drive cloud config). The mounted `/workspace` must be a git checkout whose `origin` maps to a repo already on Codacy with a finished analysis.

## Two Pipelines

**`local-pipeline.sh`** (default `CMD`): for developers. Mounts `/workspace` from host. Runs `/configure-codacy-cloud` via Claude (Haiku, `--permission-mode dontAsk`).

**`server-pipeline.sh`**: for the Active Analysis Manager (AAM) in production (k8s). Validates required env vars, clones the repo via `GIT_TOKEN` (then scrubs the token from the remote URL), runs `/configure-codacy-cloud`, sanitizes the summary, and PUT-uploads a JSONL summary to `RESULT_UPLOAD_URL` (presigned S3). Exit code 2 = upload failure; non-zero from skill = skill failure.

Additional vars required for server pipeline: `GIT_TOKEN`, `CODACY_PROVIDER` (`gh`/`ghe`/`gl`/`gle`/`bb`), `CODACY_ORG_NAME`, `CODACY_REPO_NAME`, `RESULT_UPLOAD_URL`.

To test server pipeline locally (firewall blocks git providers — skip it with `RUNNING_IN_K8S=true`):
```bash
docker run --rm -it \
-v codacy-tool-cache:/home/runner/.codacy \
-e RUNNING_IN_K8S=true \
-e CODACY_API_TOKEN -e ANTHROPIC_API_KEY -e GIT_TOKEN \
-e CODACY_PROVIDER=gh -e CODACY_ORG_NAME=your-org -e CODACY_REPO_NAME=your-repo \
-e RESULT_UPLOAD_URL=https://httpbin.org/put \
--entrypoint /usr/local/bin/server-pipeline.sh \
codacy/autoconfig
```

## Security model (OD-78)

The agent runs least-privilege so a prompt injection from the untrusted `/workspace` cannot steal a secret. Two OS users:

- **`runner` (uid 1001)** — holds the Codacy credentials (`/home/runner/.codacy`, mode 700) and runs the Anthropic auth proxy (`anthropic-proxy.js`) that holds the real `ANTHROPIC_API_KEY`.
- **`agent` (uid 1002)** — runs `claude -p`. Its environment contains **no real secret**: `ANTHROPIC_BASE_URL` points at the local proxy with a dummy token; `CODACY_API_TOKEN`/`GIT_TOKEN`/`GEMINI_API_KEY` are unset. It reaches the Codacy CLIs only through `/usr/local/bin/codacy{,-analysis}` shims that `sudo -u runner` the real binaries (renamed `*-real`).

The entrypoint runs as root: firewall → Codacy login as runner (token via env, never argv) → start proxy as runner → scrub env → `exec runuser -u agent`. Network egress is an iptables IP allowlist **plus** a dnsmasq DNS allowlist (only Anthropic + Codacy resolve; everything else is sinkholed to `0.0.0.0`, and only root may reach the upstream resolver). Claude runs on Haiku with `--permission-mode dontAsk` and a managed-settings lock (`/etc/claude-code/managed-settings.json`).

Verify with `./docker/test-hardening.sh` (adversarial probes). Probes 1–12 need no live keys; the opt-in `cli` / `e2e` probes need a throwaway Codacy account token + a Codacy-tracked git checkout. Overview: `docs/hardening-overview.md`.

## Container Architecture

**Entrypoint** (`entrypoint.sh`): firewall init (skipped when `RUNNING_IN_K8S=true`) → Codacy login as `runner` → start Anthropic proxy as `runner` → prepare shared setgid `/workspace/.codacy` → scrub env and `exec runuser -u agent -- … "$@"`.

**Firewall** (`init-firewall.sh`): iptables + ipset IP allowlist (`api.anthropic.com`, `statsig.anthropic.com`, `generativelanguage.googleapis.com`, `oauth2.googleapis.com`, `api.codacy.com`, `app.codacy.com`, `app.dev.codacy.org`, `app.staging.codacy.org`) + a local dnsmasq DNS allowlist for the same domains. Logs blocked connections via `/dev/kmsg`. In k8s, egress is handled by NetworkPolicy instead (firewall skipped).

**Skills** baked into `/home/agent/.claude/commands/`: `configure-codacy-cloud`, `configure-codacy`, `codacy-analysis-cli`, `codacy-cloud-cli`. The Dockerfile uses `ADD https://api.github.com/.../refs/heads/master` as a cache-buster so `docker build` always fetches the latest skills without `--no-cache`.

**Installed CLIs** (npm globals): `claude` (`@anthropic-ai/claude-code`), `gemini` (present but unused), `codacy` (`@codacy/codacy-cloud-cli`), `codacy-analysis` (`@codacy/analysis-cli`). Claude permissions in `claude-settings.json` are tightened (no `WebFetch`/`Glob`/`Grep`; `Read`/`Write`/`Edit` scoped to `/workspace`; secret-path + network-binary denies; Bash prefix allowlist).

**Runtimes** available for tools: Java (default-jdk-headless), Python 3, Ruby, Go 1.26, shellcheck.

**Volume** `codacy-tool-cache` → `/home/runner/.codacy`: persists downloaded tool binaries and Trivy DB across container runs.

## Updating Skills

Skills are fetched from `codacy-skills` master at build time. To pick up skill changes, rebuild:
```bash
docker compose build
```
The `ADD` cache-buster in the Dockerfile invalidates the layer when `codacy-skills` master moves.
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Set `SOURCE_PATH` in `.env` (or export it), then:
docker compose run --rm codacy-ai
```

Required env vars: `CODACY_API_TOKEN`, and `ANTHROPIC_API_KEY` or `GEMINI_API_KEY` (or both).
Required env vars: `CODACY_API_TOKEN` and `ANTHROPIC_API_KEY`.

The repository at `SOURCE_PATH` must already be on Codacy Cloud with at least one finished analysis. The container tunes
the cloud configuration via Cloud reanalysis — it does not run local analysis, and it does not import not-yet-on-Codacy
Expand All @@ -18,9 +18,9 @@ Or from any folder, without the compose file:
docker run --rm -it \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
--device /dev/kmsg:/dev/kmsg \
-v codacy-tool-cache:/home/node/.codacy \
-v codacy-tool-cache:/home/runner/.codacy \
-v $(pwd):/workspace \
-e CODACY_API_TOKEN -e ANTHROPIC_API_KEY -e GEMINI_API_KEY \
-e CODACY_API_TOKEN -e ANTHROPIC_API_KEY \
codacy/autoconfig
```

Expand All @@ -30,7 +30,7 @@ Or with an explicit env file:
docker run --rm -it \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
--device /dev/kmsg:/dev/kmsg \
-v codacy-tool-cache:/home/node/.codacy \
-v codacy-tool-cache:/home/runner/.codacy \
-v $(pwd):/workspace \
--env-file ./../.env \
codacy/autoconfig
Expand All @@ -42,7 +42,7 @@ docker run --rm -it \
| `-it` | Interactive terminal |
| `--cap-add=NET_ADMIN --cap-add=NET_RAW` | Required to enforce the outbound firewall inside the container |
| `--device /dev/kmsg:/dev/kmsg` | Kernel device needed by the firewall block-log stream |
| `-v codacy-tool-cache:/home/node/.codacy` | Persistent volume so downloaded tools survive between runs |
| `-v codacy-tool-cache:/home/runner/.codacy` | Persistent volume so downloaded tools survive between runs |
| `-v $(pwd):/workspace` | Mounts your current folder as `/workspace` |
| `-e ...` | Passes API tokens from your host environment into the container |
| `--env-file /path/to/.env` | Alternative to `-e` flags — loads vars from a file |
Expand All @@ -64,14 +64,14 @@ The image ships two entrypoint scripts:
provider (`CODACY_PROVIDER` of `gh`/`ghe` for GitHub, `gl`/`gle` for GitLab, `bb` for Bitbucket).

Both scripts run the same skill. The skill tunes a repository's Codacy Cloud configuration via Cloud reanalysis and
never runs local static analysis tools — that's why the container's egress allowlist is narrow (Claude, Gemini, Codacy).
never runs local static analysis tools — that's why the container's egress allowlist is narrow (Claude + Codacy).

To test `server-pipeline.sh` locally, override the entrypoint and provide the additional env vars. Note that the
local firewall does not allow git provider hosts, so set `RUNNING_IN_K8S=true` to skip it for this test:

```bash
docker run --rm -it \
-v codacy-tool-cache:/home/node/.codacy \
-v codacy-tool-cache:/home/runner/.codacy \
-e RUNNING_IN_K8S=true \
-e CODACY_API_TOKEN \
-e ANTHROPIC_API_KEY \
Expand Down Expand Up @@ -109,7 +109,10 @@ Required env vars for the server pipeline: `CODACY_API_TOKEN`, `ANTHROPIC_API_KE

- `codacy` — Codacy Cloud CLI
- `codacy-analysis` — Codacy Analysis CLI (used by the skill only for config-file operations)
- `claude` / `gemini` — AI assistants
- Java 21, Python 3.12, Ruby, Go 1.26, shellcheck
- Outbound firewall — allowlist for Claude, Gemini, and Codacy only. In production (k8s) the firewall is skipped and
egress is enforced by NetworkPolicy at the cluster level instead.
- `claude` — AI assistant (runs on Haiku, `--permission-mode dontAsk`). `gemini` is installed but no longer used.
- Java, Python 3, Ruby, Go 1.26, shellcheck
- Outbound firewall — IP allowlist plus a DNS allowlist (Claude + Codacy hosts, incl. `app.dev`/`app.staging.codacy.org`);
non-allowlisted DNS is sinkholed. In production (k8s) the firewall is skipped and egress is enforced by NetworkPolicy.
- **Least-privilege agent (OD-78):** two OS users — `runner` holds the secrets (Codacy credentials + an Anthropic auth
proxy), `agent` runs Claude with no readable secret and reaches the Codacy CLIs only through `sudo` shims. So a
prompt-injected agent has nothing to exfiltrate. See `docs/hardening-overview.md`; verify with `./docker/test-hardening.sh`.
5 changes: 2 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ services:
devices:
- /dev/kmsg:/dev/kmsg
volumes:
# Persist tool installations and Trivy DB across runs
- codacy-tool-cache:/home/node/.codacy
# Persist tool installations and Trivy DB across runs (owned by the runner user)
- codacy-tool-cache:/home/runner/.codacy
# Mount the repo to analyse — override with SOURCE_PATH=/path/to/repo
- ${SOURCE_PATH:-.}:/workspace
working_dir: /workspace
environment:
- CODACY_API_TOKEN
- ANTHROPIC_API_KEY
- GEMINI_API_KEY
- JAVA_OPTS=-Xmx1g
stdin_open: true
tty: true
Expand Down
63 changes: 49 additions & 14 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-reco
ipset \
iproute2 \
dnsutils \
dnsmasq \
aggregate \
# Utilities
curl \
Expand Down Expand Up @@ -42,32 +43,66 @@ RUN npm install -g \
@codacy/codacy-cloud-cli \
@codacy/analysis-cli

# --- Privilege separation ------------------------------------------------
# runner (1001): owns credentials + the Anthropic auth proxy; runs the real CLIs.
# agent (1002): runs claude; holds no secret. Shared group `codacy` lets both
# read/write /workspace/.codacy via setgid (Task 7).
RUN groupadd -g 1003 codacy \
&& useradd -m -u 1001 -g codacy runner \
&& useradd -m -u 1002 -g codacy agent \
# Relocate the real Codacy CLIs to <name>-real in the SAME dir so their
# relative npm symlinks stay valid; install shims at the original names that
# elevate to runner.
&& mv /usr/local/bin/codacy /usr/local/bin/codacy-real \
&& mv /usr/local/bin/codacy-analysis /usr/local/bin/codacy-analysis-real
COPY docker/codacy-shim.sh /usr/local/bin/codacy
COPY docker/codacy-run.sh /usr/local/bin/codacy-run
RUN cp /usr/local/bin/codacy /usr/local/bin/codacy-analysis \
&& chmod +x /usr/local/bin/codacy /usr/local/bin/codacy-analysis /usr/local/bin/codacy-run \
# Codacy credentials live in runner's home, unreadable by agent.
&& mkdir -p /home/runner/.codacy \
&& chown -R runner:codacy /home/runner/.codacy \
&& chmod 700 /home/runner/.codacy \
# /workspace is owned by `agent` (so server mode can clone into it) but the
# Codacy CLI runs as `runner` and auto-detects the repo via git — git's
# dubious-ownership guard would otherwise reject the agent-owned checkout.
&& git config --system --add safe.directory /workspace

# Pre-bake skills — Claude loads via --plugin-dir, Gemini installs from local path
# ADD'ing the master ref content makes Docker invalidate this layer whenever codacy-skills master moves,
# so a fresh `docker build` always gets the latest skills without --no-cache.
ADD https://api.github.com/repos/codacy/codacy-skills/git/refs/heads/master /tmp/codacy-skills-ref
RUN git clone --depth 1 https://github.com/codacy/codacy-skills.git /opt/codacy-skills

COPY docker/anthropic-proxy.js /usr/local/bin/anthropic-proxy.js
COPY docker/init-firewall.sh /usr/local/bin/init-firewall.sh
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY docker/local-pipeline.sh /usr/local/bin/local-pipeline.sh
COPY docker/server-pipeline.sh /usr/local/bin/server-pipeline.sh
RUN chmod +x /usr/local/bin/init-firewall.sh /usr/local/bin/entrypoint.sh /usr/local/bin/local-pipeline.sh /usr/local/bin/server-pipeline.sh \
&& printf 'node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh\nnode ALL=(root) NOPASSWD: /bin/chown -R node\\:node /home/node/.codacy\n' \
> /etc/sudoers.d/node-firewall \
&& chmod 0440 /etc/sudoers.d/node-firewall
COPY docker/summary-sanitize.sh /usr/local/bin/summary-sanitize.sh
RUN chmod +x /usr/local/bin/init-firewall.sh /usr/local/bin/entrypoint.sh /usr/local/bin/local-pipeline.sh /usr/local/bin/server-pipeline.sh /usr/local/bin/summary-sanitize.sh \
# The agent may run ONLY the runner-side CLI launcher, and only as runner.
&& printf 'agent ALL=(runner) NOPASSWD: /usr/local/bin/codacy-run\n' \
> /etc/sudoers.d/agent-cli \
&& chmod 0440 /etc/sudoers.d/agent-cli

USER node
# Image starts as root; entrypoint performs setup then drops to `agent`.
USER root

# Install skills into ~/.claude/commands/ — claude reads these natively without --plugin-dir,
# which avoids the v2.1.123 regression where --plugin-dir silently fails in stream-json mode
COPY --chown=node:node docker/claude-settings.json /home/node/.claude/settings.json
RUN mkdir -p /home/node/.claude/commands/references \
&& cp /opt/codacy-skills/skills/configure-codacy/SKILL.md /home/node/.claude/commands/configure-codacy.md \
&& cp /opt/codacy-skills/skills/configure-codacy-cloud/SKILL.md /home/node/.claude/commands/configure-codacy-cloud.md \
&& cp /opt/codacy-skills/skills/codacy-analysis-cli/SKILL.md /home/node/.claude/commands/codacy-analysis-cli.md \
&& cp /opt/codacy-skills/skills/codacy-cloud-cli/SKILL.md /home/node/.claude/commands/codacy-cloud-cli.md \
&& cp /opt/codacy-skills/skills/codacy-analysis-cli/references/* /home/node/.claude/commands/references/
# Install skills into the agent's ~/.claude/commands/ — claude reads these natively
# without --plugin-dir, which avoids the v2.1.123 regression where --plugin-dir
# silently fails in stream-json mode. Managed settings lock the policy so the
# repo/agent cannot widen it.
COPY --chown=agent:codacy docker/claude-settings.json /home/agent/.claude/settings.json
COPY docker/managed-settings.json /etc/claude-code/managed-settings.json
RUN mkdir -p /home/agent/.claude/commands/references \
&& cp /opt/codacy-skills/skills/configure-codacy/SKILL.md /home/agent/.claude/commands/configure-codacy.md \
&& cp /opt/codacy-skills/skills/configure-codacy-cloud/SKILL.md /home/agent/.claude/commands/configure-codacy-cloud.md \
&& cp /opt/codacy-skills/skills/codacy-analysis-cli/SKILL.md /home/agent/.claude/commands/codacy-analysis-cli.md \
&& cp /opt/codacy-skills/skills/codacy-cloud-cli/SKILL.md /home/agent/.claude/commands/codacy-cloud-cli.md \
&& cp /opt/codacy-skills/skills/codacy-analysis-cli/references/* /home/agent/.claude/commands/references/ \
&& chown -R agent:codacy /home/agent/.claude \
&& chmod 0644 /etc/claude-code/managed-settings.json

WORKDIR /workspace

Expand Down
32 changes: 32 additions & 0 deletions docker/anthropic-proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Minimal localhost proxy. Holds the real Anthropic API key in THIS process's
// environment (owned by `runner`) and injects it into every upstream request,
// overwriting whatever dummy credential the agent sent. The agent (a different
// UID) cannot read this process's /proc/<pid>/environ, so the key stays secret.
const http = require('http');
const https = require('https');

const PORT = parseInt(process.env.ANTHROPIC_PROXY_PORT || '8118', 10);
const REAL_KEY = process.env.ANTHROPIC_API_KEY;
const UPSTREAM = 'api.anthropic.com';

if (!REAL_KEY) {
console.error('anthropic-proxy: ANTHROPIC_API_KEY not set; refusing to start');
process.exit(1);
}

const server = http.createServer((req, res) => {
const headers = { ...req.headers, host: UPSTREAM };
// Replace any client-supplied auth with the real key.
delete headers['authorization'];
headers['x-api-key'] = REAL_KEY;
headers['anthropic-version'] = headers['anthropic-version'] || '2023-06-01';

const upstream = https.request(
{ hostname: UPSTREAM, port: 443, path: req.url, method: req.method, headers },
(up) => { res.writeHead(up.statusCode, up.headers); up.pipe(res); }
);
upstream.on('error', (e) => { res.writeHead(502); res.end('proxy error: ' + e.message); });
req.pipe(upstream);
});

server.listen(PORT, '127.0.0.1', () => console.error(`anthropic-proxy listening on 127.0.0.1:${PORT}`));
26 changes: 20 additions & 6 deletions docker/claude-settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@
"permissions": {
"allow": [
"Bash(*)",
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"WebFetch(*)"
"Read(/workspace/**)",
"Write(/workspace/**)",
"Edit(/workspace/**)"
],
"deny": [
"Read(/home/runner/**)",
"Read(//home/runner/**)",
"Read(/run/codacy/**)",
"Read(//run/codacy/**)",
"Read(/proc/**)",
"Read(//proc/**)",
"Read(/etc/sudoers.d/**)",
Comment on lines +10 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

Potential Security Bypass: Inconsistent path format in Read deny rules

In claude-settings.json, some deny rules use double slashes (e.g., //run/codacy/** and //proc/**) because Claude Code uses double slashes for absolute paths. However, other rules like /home/runner/** and /etc/sudoers.d/** only use a single slash.

If Claude Code indeed normalizes absolute paths to start with //, the single-slash rules might be ineffective, allowing the agent to read those sensitive directories.

Remediation:
Use the double-slash format consistently for all absolute path deny rules, or include both formats to ensure complete coverage.

Suggested change
"Read(/home/runner/**)",
"Read(//run/codacy/**)",
"Read(//proc/**)",
"Read(/etc/sudoers.d/**)",
"Read(//home/runner/**)",
"Read(//run/codacy/**)",
"Read(//proc/**)",
"Read(//etc/sudoers.d/**)",

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: each secret-path Read deny now lists both the single- and double-slash form (/home/runner/** and //home/runner/**, etc.) so it matches regardless of how Claude Code normalizes absolute paths. Note these denies are defense-in-depth — the load-bearing protection is OS perms (700 runner-owned dirs, distinct uid), which block the agent even if a rule format is off.

"Read(//etc/sudoers.d/**)",
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(ssh:*)",
"Bash(dig:*)",
"Bash(nslookup:*)",
"Bash(host:*)",
"Bash(ping:*)"
]
}
}
20 changes: 20 additions & 0 deletions docker/codacy-run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Runner-side launcher for the Codacy CLIs. Loads the Codacy token from a
# runner-only file into the environment (the CLI reads CODACY_API_TOKEN at
# runtime — no persisted login needed) and execs the real CLI. Invoked as
# `runner` via the sudo shim; the agent (a different uid) cannot read the token
# file (600, runner-owned) nor this process's /proc environ.
set -euo pipefail
name="$1"; shift
# Allowlist the CLI name — the agent reaches this via a sudo rule that permits
# any arguments, so without this an attacker could pass a traversal path
# (e.g. ../../workspace/evil) to run an arbitrary binary as `runner` with the
# token loaded. Only the two real Codacy CLIs are permitted.
case "$name" in
codacy|codacy-analysis) ;;
*) echo "codacy-run: unauthorized CLI name '$name'" >&2; exit 1 ;;
esac
if [ -f /run/codacy/codacy.env ]; then
set -a; . /run/codacy/codacy.env; set +a
fi
exec "/usr/local/bin/${name}-real" "$@"
Comment on lines +8 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

Critical Security Vulnerability: Arbitrary Code Execution / Privilege Escalation

The codacy-run.sh script executes /usr/local/bin/${name}-real where name is the first argument passed from the unprivileged agent user via sudo. Because there is no validation or sanitization on name, an attacker can use directory traversal (e.g., ../../workspace/evil) to execute an arbitrary executable located in the writable /workspace directory as the runner user.

This completely bypasses the privilege separation boundary, allowing a compromised agent to run arbitrary commands as runner and exfiltrate the CODACY_API_TOKEN or ANTHROPIC_API_KEY.

Remediation:
Strictly validate that name is either codacy or codacy-analysis before executing it.

Suggested change
name="$1"; shift
if [ -f /run/codacy/codacy.env ]; then
set -a; . /run/codacy/codacy.env; set +a
fi
exec "/usr/local/bin/${name}-real" "$@"
name="$1"; shift
if [[ "$name" != "codacy" && "$name" != "codacy-analysis" ]]; then
echo "ERROR: Unauthorized CLI name: $name" >&2
exit 1
fi
if [ -f /run/codacy/codacy.env ]; then
set -a; . /run/codacy/codacy.env; set +a
fi
exec "/usr/local/bin/${name}-real" "$@"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9e-prefix commit: codacy-run.sh now allowlists the CLI name to exactly codacy/codacy-analysis (case statement) and exits non-zero otherwise, so the traversal path you showed is rejected before exec. Verified: sudo -u runner codacy-run ../../workspace/evil → "unauthorized CLI name"; legit codacy still works.

Loading