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
4 changes: 2 additions & 2 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
run: make coverage-sonar

- name: SonarCloud scan
uses: SonarSource/sonarcloud-github-action@v5
uses: SonarSource/sonarcloud-github-action@ffc3010689be73b8e5ae0c57ce35968afd7909e8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Expand All @@ -43,7 +43,7 @@ jobs:
-Dsonar.enableIssueAnnotation=true

- name: SonarCloud quality gate
uses: SonarSource/sonarqube-quality-gate-action@v1.2.0
uses: SonarSource/sonarqube-quality-gate-action@cf038b0e0cdecfa9e56c198bbb7d21d751d62c3b
with:
scanMetadataReportFile: dist/quality/sonar/scannerwork/report-task.txt
env:
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ kill:

# Quality and Security targets
.PHONY: sonar sonar-cloud coverage-sonar sbom sbom-upload gitleaks fmt lint vet
.PHONY: docker-validate

sonar: ## Run sonar-scanner for SonarQube analysis
@SONAR_DIR=$(SONAR_DIR) COVERAGE_DIR=$(COVERAGE_DIR) VERSION=$(VERSION) ./scripts/run-sonar.sh
Expand Down Expand Up @@ -146,6 +147,9 @@ vet: ## Run type checking with mypy
echo "mypy not found. Install with: pip install mypy"; \
fi

docker-validate: ## Build Docker image and validate docker-compose
@./scripts/validate-docker.sh

help:
@echo "Claude Code API Commands:"
@echo ""
Expand Down
103 changes: 103 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y \
bash \
ca-certificates \
curl \
git \
jq \
python3 \
python3-pip \
python3-venv \
sudo \
&& rm -rf /var/lib/apt/lists/*

# Create non-root user
RUN useradd -m -s /bin/bash claudeuser && \
echo "claudeuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
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

Granting unrestricted NOPASSWD sudo privileges to the claudeuser in the Dockerfile allows any process in the container to escalate to root, significantly increasing the impact of a compromise and negating the security benefits of running as a non-root user. The application should be designed to run without requiring root privileges. If sudo is essential for a specific setup command, it should be used restrictively and then removed.

Comment on lines +18 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Passwordless sudo grants excessive privileges.

Granting NOPASSWD:ALL sudo access to the container user is overly permissive. If sudo is needed for specific operations, consider limiting it to those specific commands.

🔒 Proposed restriction

If sudo isn't actually required at runtime, remove it entirely:

 RUN useradd -m -s /bin/bash claudeuser && \
-    echo "claudeuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
+    true

Or if specific commands need elevated privileges, restrict to those:

-    echo "claudeuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
+    echo "claudeuser ALL=(ALL) NOPASSWD: /usr/bin/apt-get" >> /etc/sudoers
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RUN useradd -m -s /bin/bash claudeuser && \
echo "claudeuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
RUN useradd -m -s /bin/bash claudeuser && \
true
🤖 Prompt for AI Agents
In `@docker/Dockerfile` around lines 18 - 19, The Dockerfile currently creates a
user with passwordless sudo by running useradd -m -s /bin/bash claudeuser and
appending "claudeuser ALL=(ALL) NOPASSWD:ALL" to /etc/sudoers; remove or tighten
that sudo grant: either delete the sudoers append entirely if sudo is not
required at runtime, or replace NOPASSWD:ALL with a minimal, explicit command
list allowed for claudeuser (e.g., restrict to the specific binaries needed) and
keep explicit password requirement otherwise; update the clauses written to
/etc/sudoers accordingly and ensure user creation (useradd claudeuser) still
succeeds without granting excessive privileges.


# Set up application directory
WORKDIR /home/claudeuser/app
COPY . /home/claudeuser/app
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The COPY . /home/claudeuser/app instruction copies the entire build context at once. This is inefficient for Docker's layer caching, as any change to any file will invalidate this layer and cause subsequent layers to be rebuilt. It can also increase image size by copying unnecessary files (e.g., .git, build artifacts).

To improve this:

  1. Use a .dockerignore file to exclude unneeded files and directories.
  2. Copy files in stages to better leverage caching. First, copy only dependency manifest files (pyproject.toml, etc.), then install dependencies, and finally copy the rest of your application source code. This will make subsequent builds much faster.

RUN chown -R claudeuser:claudeuser /home/claudeuser/app

USER claudeuser

# Install Claude CLI using the official installer (no npm required)
RUN curl -fsSL https://claude.ai/install.sh | bash
Comment on lines +28 to +29
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 suggestion (security): Tighten security around running remote install script via curl | bash

Piping a remote script directly into bash during image build is a supply‑chain risk because it trusts the live contents of https://claude.ai/install.sh. Where possible, pin to a specific version (tagged URL), verify a checksum/signature, or download the script first and record/inspect its version before execution to improve reproducibility and reduce exposure to upstream changes.

Suggested implementation:

USER claudeuser

# Install Claude CLI using the official installer (no npm required)
# The installer URL and checksum are build‑time arguments so they can be pinned for reproducibility.
ARG CLAUDE_INSTALL_URL="https://claude.ai/install.sh"
ARG CLAUDE_INSTALL_SHA256=""
RUN curl -fsSL "$CLAUDE_INSTALL_URL" -o /tmp/claude_install.sh && \
    if [ -n "$CLAUDE_INSTALL_SHA256" ]; then \
        echo "$CLAUDE_INSTALL_SHA256  /tmp/claude_install.sh" | sha256sum -c -; \
    else \
        echo "WARNING: CLAUDE_INSTALL_SHA256 not set; skipping checksum verification" >&2; \
    fi && \
    bash /tmp/claude_install.sh && \
    rm /tmp/claude_install.sh

To fully leverage this change:

  1. In your CI/build pipeline, set --build-arg CLAUDE_INSTALL_URL=... to a pinned, versioned installer URL if the project provides one.
  2. Compute and pass a fixed --build-arg CLAUDE_INSTALL_SHA256=... for that script version to enforce checksum verification and ensure reproducible, tamper‑evident builds.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Piping the output of curl directly to bash is a security risk. If the download is compromised (e.g., through a man-in-the-middle attack), it could lead to arbitrary code execution. It is safer to download the script to a file first, optionally verify its integrity if a checksum is available, and then execute it.

RUN curl -fsSL -o /tmp/install.sh https://claude.ai/install.sh && \
    bash /tmp/install.sh && \
    rm /tmp/install.sh


# Create virtualenv and install dependencies
RUN python3 -m venv /home/claudeuser/venv && \
/home/claudeuser/venv/bin/pip install --upgrade pip setuptools wheel && \
/home/claudeuser/venv/bin/pip install -e . --use-pep517 || \
/home/claudeuser/venv/bin/pip install -e .

ENV PATH="/home/claudeuser/venv/bin:/home/claudeuser/.local/bin:/home/claudeuser/.bun/bin:${PATH}"

# Create Claude config and workspace directories
RUN mkdir -p /home/claudeuser/.config/claude /home/claudeuser/app/workspace

EXPOSE 8000

ENV HOST=0.0.0.0
ENV PORT=8000

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1

RUN cat <<'EOF' > /home/claudeuser/entrypoint.sh
#!/usr/bin/env bash
set -euo pipefail

auth_ready=false

# Prefer host-mounted Claude auth (Claude Max / Claude Code)
if [ -d "$HOME/.claude" ] && [ -n "$(ls -A "$HOME/.claude" 2>/dev/null)" ]; then
echo "Using Claude auth from $HOME/.claude"
auth_ready=true
fi

# Fallback to Claude config file if present
if [ "$auth_ready" != "true" ] && [ -f "$HOME/.config/claude/config.json" ]; then
echo "Using Claude config from $HOME/.config/claude/config.json"
auth_ready=true
fi

# Optionally write config from API key (only when explicitly requested)
if [ "$auth_ready" != "true" ] && [ -n "${ANTHROPIC_API_KEY:-}" ] && [ "${USE_CLAUDE_MAX:-}" != "true" ]; then
if [ "${WRITE_CLAUDE_CONFIG:-}" = "true" ]; then
echo "Configuring Claude Code with API key..."
python3 - <<'PY'
import json
import os
from pathlib import Path

config_dir = Path.home() / ".config" / "claude"
config_dir.mkdir(parents=True, exist_ok=True)
with (config_dir / "config.json").open("w", encoding="utf-8") as handle:
json.dump({"apiKey": os.environ["ANTHROPIC_API_KEY"], "autoUpdate": False}, handle)
PY
echo "Claude Code configured with API key"
auth_ready=true
else
echo "ANTHROPIC_API_KEY is set but WRITE_CLAUDE_CONFIG is not true."
echo "For security, no config file was written. Mount ~/.claude or ~/.config/claude or set WRITE_CLAUDE_CONFIG=true."
fi
fi

if [ "$auth_ready" != "true" ] && [ "${USE_CLAUDE_MAX:-}" = "true" ]; then
echo "Using Claude Max subscription - please run: docker exec -it claude-code-api claude"
echo "Then authenticate via browser when prompted"
elif [ "$auth_ready" != "true" ]; then
echo "No authentication configured. Mount ~/.claude or ~/.config/claude, or set ANTHROPIC_API_KEY + WRITE_CLAUDE_CONFIG=true."
fi

echo "Starting API server..."
cd /home/claudeuser/app
exec python3 -m claude_code_api.main
EOF
RUN chmod +x /home/claudeuser/entrypoint.sh

ENTRYPOINT ["/home/claudeuser/entrypoint.sh"]
35 changes: 35 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
services:
claude-code-api:
build:
context: ..
dockerfile: docker/Dockerfile
container_name: claude-code-api
ports:
- "127.0.0.1:8000:8000" # API server port
- "127.0.0.1:8888:8888" # OAuth callback proxy port
Comment on lines +7 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question: Re-evaluate exposing port 8888 on the container for the OAuth proxy

Given the proxy runs as a separate host-side process (docker/oauth-proxy.py), the container should be calling host.docker.internal:8888 rather than binding to 8888 itself. Since nothing in the container appears to listen on 8888, this port mapping may be unnecessary or misleading. Consider removing it or documenting why it’s needed.

environment:
# Use Claude Max subscription instead of API key
- USE_CLAUDE_MAX=true
- HOST=0.0.0.0
- PORT=8000
# Optional: Project root for Claude Code workspace
- CLAUDE_PROJECT_ROOT=/home/claudeuser/app/workspace
# OAuth proxy configuration
- OAUTH_PROXY_HOST=host.docker.internal
- OAUTH_PROXY_PORT=8888
Comment on lines +18 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

host.docker.internal may not resolve on native Linux Docker.

host.docker.internal is available by default on Docker Desktop (Mac/Windows) but requires manual configuration on native Linux Docker. Consider adding a note in documentation or using extra_hosts to ensure compatibility.

🔧 Proposed fix for Linux compatibility
     environment:
       # Use Claude Max subscription instead of API key
       - USE_CLAUDE_MAX=true
       - HOST=0.0.0.0
       - PORT=8000
       # Optional: Project root for Claude Code workspace
       - CLAUDE_PROJECT_ROOT=/home/claudeuser/app/workspace
       # OAuth proxy configuration
       - OAUTH_PROXY_HOST=host.docker.internal
       - OAUTH_PROXY_PORT=8888
+    extra_hosts:
+      - "host.docker.internal:host-gateway"
🤖 Prompt for AI Agents
In `@docker/docker-compose.yml` around lines 18 - 19, The compose environment
variables OAUTH_PROXY_HOST and OAUTH_PROXY_PORT rely on host.docker.internal
which may not resolve on native Linux; update the compose service that sets
OAUTH_PROXY_HOST to either (a) add an extra_hosts entry mapping
host.docker.internal to host-gateway (so Docker resolves it on Linux) or (b)
replace the env with a configurable host value and document that Linux users
must set it or add the equivalent extra_hosts mapping; reference the
OAUTH_PROXY_HOST and OAUTH_PROXY_PORT environment entries and ensure the chosen
approach is documented so Linux Docker users can connect to the host.

volumes:
# Mount workspace for persistent projects
- ../workspace:/home/claudeuser/app/workspace
# Mount local Claude auth (Claude Max / Claude Code)
- ${HOME}/.claude:/home/claudeuser/.claude
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

${HOME} may be undefined in non-interactive contexts.

When running from CI pipelines, systemd services, or cron jobs, ${HOME} might not be set. Consider using a fallback or documenting this requirement.

🤖 Prompt for AI Agents
In `@docker/docker-compose.yml` at line 24, The volume mapping in
docker-compose.yml uses ${HOME}/.claude which can be unset in non-interactive
contexts; update the volume declaration to use a safe fallback or explicit env
var (e.g., use a default like ${HOME:-/root} or introduce and reference a
PROJECT_USER_HOME env var) and document the required env var in the README;
adjust the mapping line that currently contains "-
${HOME}/.claude:/home/claudeuser/.claude" to reference the chosen
fallback/variable so CI, systemd, and cron jobs won't fail when HOME is
undefined.

# Mount Claude config to persist authentication
- ../claude-config:/home/claudeuser/.config/claude
# Optional: Mount custom config
- ../config:/home/claudeuser/app/config
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
Comment on lines +30 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The healthcheck is defined in both the Dockerfile and this docker-compose.yml file. Docker Compose's definition will override the one in the Dockerfile, but this duplication can lead to confusion and inconsistencies (e.g., the start_period is different). It's best practice to define it in only one place. Since compose files are often environment-specific, defining it here is appropriate. I recommend removing the HEALTHCHECK instruction from the Dockerfile to have a single source of truth.

Loading
Loading