Skip to content
Open
Show file tree
Hide file tree
Changes from 16 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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -328,4 +328,5 @@ tests/__pycache__/
docs/start.md
docs/typescript-translation-plan.md

# Note: test scripts in tests/ directory should be tracked in git
# Note: test scripts in tests/ directory should be tracked in git
fix_env.patch
103 changes: 103 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Use Ubuntu as base for better Claude Code support
FROM ubuntu:22.04

# Prevent interactive prompts during build
ENV DEBIAN_FRONTEND=noninteractive

# Install system dependencies including Node.js
RUN apt-get update && apt-get install -y \
curl \
git \
python3.10 \
python3-pip \
ca-certificates \
bash \
sudo \
jq \
&& rm -rf /var/lib/apt/lists/*

# Upgrade pip and setuptools to latest versions
RUN pip3 install --upgrade pip setuptools wheel

# Install Node.js 18+ (required for Claude Code)
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
apt-get install -y nodejs && \
node --version && npm --version
Comment on lines +23 to +25

Choose a reason for hiding this comment

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

medium

The apt-get install command for nodejs does not clean up the apt cache afterwards. This increases the image size unnecessarily. It's a good practice to chain apt-get install with rm -rf /var/lib/apt/lists/* in the same RUN layer to keep the layer small.

RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
    apt-get install -y nodejs && \
    rm -rf /var/lib/apt/lists/* && \
    node --version && npm --version


# Install Claude Code CLI globally as root (before switching to non-root user)
RUN npm install -g @anthropic-ai/claude-code && \
claude --version

# Create non-root user for running Claude Code
RUN useradd -m -s /bin/bash claudeuser && \
echo "claudeuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
Comment on lines +32 to +33

Choose a reason for hiding this comment

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

high

The non-root user claudeuser is granted passwordless sudo access to all commands. This is a significant security risk and violates the principle of least privilege. The container should be designed so that the application user does not require sudo at all. Based on the rest of the Dockerfile and the application, sudo does not appear to be necessary for the user at runtime.

RUN useradd -m -s /bin/bash claudeuser

Comment on lines +32 to +33
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Overly permissive sudo access.

Granting claudeuser passwordless sudo with ALL=(ALL) NOPASSWD:ALL is a significant security risk. If the Claude Code process or API is compromised, an attacker gains full root access to the container.

Limit sudo privileges to only what's necessary, or remove sudo access entirely if not required:

 RUN useradd -m -s /bin/bash claudeuser && \
-    echo "claudeuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
+    # Grant limited sudo only if needed, e.g., for specific commands
+    echo "claudeuser ALL=(ALL) NOPASSWD: /usr/bin/apt-get" >> /etc/sudoers

Or remove the line entirely if sudo is not required at runtime.

🤖 Prompt for AI Agents
In Dockerfile around lines 32-33, the Dockerfile grants claudeuser unrestricted
passwordless sudo (ALL=(ALL) NOPASSWD:ALL), which is overly permissive; either
remove the sudoers line entirely if sudo is not required at runtime, or replace
it with a minimal sudoers entry that only permits the specific commands the
process needs (using a dedicated sudoers file under /etc/sudoers.d and
visudo-safe syntax), and avoid NOPASSWD unless absolutely necessary (require a
password or use capability-based alternatives); update the Dockerfile to create
a limited sudoers file or drop sudo usage accordingly.


# Switch to non-root user
USER claudeuser
WORKDIR /home/claudeuser

# Create Claude config directory
RUN mkdir -p /home/claudeuser/.config/claude

# Create workspace directory for Claude Code
RUN mkdir -p /home/claudeuser/workspace

# Set up working directory
WORKDIR /home/claudeuser/app

# Clone claude-code-api
RUN git clone https://github.com/christag/claude-code-api.git .

Choose a reason for hiding this comment

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

critical

The Docker image is built by cloning the repository from GitHub using git clone. This is not a good practice as it makes the build dependent on network availability and the state of the remote repository, which harms reproducibility. It also prevents building local changes. The source code should be copied from the build context using the COPY instruction. You should also consider adding a .dockerignore file to exclude unnecessary files and directories (like .git, Dockerfile, etc.) from the build context.

COPY . .

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify the repository source.

The Dockerfile clones from https://github.com/christag/claude-code-api.git, which appears to be a personal fork. Ensure this is the intended source and consider:

  1. Using the official/canonical repository if available
  2. Pinning to a specific commit SHA or tag for reproducibility and security
  3. Verifying the clone integrity

🏁 Script executed:

#!/bin/bash
# Check if this is a fork and identify the upstream repository
gh repo view christag/claude-code-api --json parent,isFork,nameWithOwner

Length of output: 169


Target official upstream and pin revision in Dockerfile
Replace the clone URL on line 49 to the canonical codingworkflow/claude-code-api (or your intended source) and pin to a specific commit SHA or tag for reproducible, secure builds.

🤖 Prompt for AI Agents
In Dockerfile around line 49, the repository is cloned from an unofficial URL;
replace the clone command to target the official upstream (e.g.,
https://github.com/codingworkflow/claude-code-api.git) and pin to a specific
commit SHA or tag for reproducible builds: update the clone to reference the
canonical repo and a fixed revision (use --branch <tag> or clone then git
checkout <commit-sha>), or add a build ARG for the REVISION and use that to
check out the exact tag/commit; ensure the final image build uses the pinned
revision rather than the branch head.


# Install dependencies using modern pip (avoiding deprecated setup.py)
RUN pip3 install --user --upgrade pip && \
pip3 install --user -e . --use-pep517 || \
pip3 install --user -e .

# Add user's local bin to PATH
ENV PATH="/home/claudeuser/.local/bin:${PATH}"

# Expose API port
EXPOSE 8000

# Environment variables (set these at runtime)
ENV ANTHROPIC_API_KEY=""
ENV HOST=0.0.0.0
ENV PORT=8000

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

# Create entrypoint script - DO NOT configure with API key if using Claude Max
RUN echo '#!/bin/bash\n\
set -e\n\
\n\
# Only configure API key if explicitly provided and not using Claude Max\n\
if [ -n "$ANTHROPIC_API_KEY" ] && [ "$USE_CLAUDE_MAX" != "true" ]; then\n\
echo "Configuring Claude Code with API key..."\n\
mkdir -p ~/.config/claude\n\

Choose a reason for hiding this comment

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

medium

The command mkdir -p ~/.config/claude inside the entrypoint script is redundant because this directory is already created at line 40 of the Dockerfile. This line can be removed from the script to avoid redundancy.

cat > ~/.config/claude/config.json << EOF\n\
{\n\
"apiKey": "$ANTHROPIC_API_KEY",\n\
"autoUpdate": false\n\
}\n\
EOF\n\
echo "Claude Code configured with API key"\n\
elif [ "$USE_CLAUDE_MAX" = "true" ]; then\n\
echo "Using Claude Max subscription - please run: docker exec -it claude-code-api claude"\n\
echo "Then authenticate via browser when prompted"\n\
else\n\
echo "No authentication configured. Set ANTHROPIC_API_KEY or USE_CLAUDE_MAX=true"\n\
fi\n\
\n\
# Test Claude Code\n\
echo "Testing Claude Code..."\n\
claude --version || echo "Claude Code installed"\n\
\n\
echo "Starting API server..."\n\
cd /home/claudeuser/app\n\
exec python3 -m claude_code_api.main' > /home/claudeuser/entrypoint.sh && \
chmod +x /home/claudeuser/entrypoint.sh
Comment on lines +72 to +100

Choose a reason for hiding this comment

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

medium

The entrypoint script is created using a long echo command. This makes the script difficult to read, maintain, and debug. It's better practice to have the entrypoint script as a separate file in the repository (e.g., entrypoint.sh) and use the COPY instruction to add it to the image.


# Start the API server with entrypoint
ENTRYPOINT ["/home/claudeuser/entrypoint.sh"]
4 changes: 2 additions & 2 deletions claude_code_api/api/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ async def create_chat_completion(
# Return streaming response
return StreamingResponse(
create_sse_response(claude_session_id, claude_model, claude_process),
media_type="text/plain",
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
Expand Down Expand Up @@ -278,7 +278,7 @@ async def create_chat_completion(
response_size=len(str(response))
)

return response
return JSONResponse(content=response, media_type="application/json")

except HTTPException:
# Re-raise HTTP exceptions
Expand Down
11 changes: 9 additions & 2 deletions claude_code_api/core/claude_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,19 @@ async def start(
*cmd,
cwd=src_dir,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
stderr=asyncio.subprocess.PIPE,
env=os.environ.copy()
)

# Wait for process to complete and capture all output
stdout, stderr = await self.process.communicate()

# Log stdout and stderr for debugging
# Debug logging MUST be here, before any error checking
logger.error(f"=== DEBUG: Exit code: {self.process.returncode} ===")
if stderr:
logger.error(f"=== DEBUG: Claude stderr: {stderr.decode()} ===")
if stdout:
logger.info(f"=== DEBUG: Claude stdout: {stdout.decode()[:1000]} ===")
Comment on lines +80 to +84

Choose a reason for hiding this comment

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

medium

The new debug log messages are using f-strings. Since the project uses structlog, it's better to pass the dynamic parts as keyword arguments to the logger methods. This allows for better processing, filtering, and querying of logs in a structured format.

Suggested change
logger.error(f"=== DEBUG: Exit code: {self.process.returncode} ===")
if stderr:
logger.error(f"=== DEBUG: Claude stderr: {stderr.decode()} ===")
if stdout:
logger.info(f"=== DEBUG: Claude stdout: {stdout.decode()[:1000]} ===")
logger.error("=== DEBUG: Claude process finished ===", exit_code=self.process.returncode)
if stderr:
logger.error("=== DEBUG: Claude stderr ===", stderr=stderr.decode())
if stdout:
logger.info("=== DEBUG: Claude stdout ===", stdout=stdout.decode()[:1000])

Comment on lines +78 to +84
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use appropriate log level for debug messages.

Lines 80, 82, and 84 use logger.error() for debug output. Debug messages should use logger.debug() to avoid polluting error logs and triggering unnecessary alerts.

Apply this diff:

-            # Log stdout and stderr for debugging
-            # Debug logging MUST be here, before any error checking
-            logger.error(f"=== DEBUG: Exit code: {self.process.returncode} ===")
+            # Debug logging before error checking
+            logger.debug(f"Exit code: {self.process.returncode}")
             if stderr:
-                logger.error(f"=== DEBUG: Claude stderr: {stderr.decode()} ===")
+                logger.debug(f"Claude stderr: {stderr.decode()}")
             if stdout:
-                logger.info(f"=== DEBUG: Claude stdout: {stdout.decode()[:1000]} ===")
+                logger.debug(f"Claude stdout (first 1000 chars): {stdout.decode()[:1000]}")
📝 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
# Log stdout and stderr for debugging
# Debug logging MUST be here, before any error checking
logger.error(f"=== DEBUG: Exit code: {self.process.returncode} ===")
if stderr:
logger.error(f"=== DEBUG: Claude stderr: {stderr.decode()} ===")
if stdout:
logger.info(f"=== DEBUG: Claude stdout: {stdout.decode()[:1000]} ===")
# Debug logging before error checking
logger.debug(f"Exit code: {self.process.returncode}")
if stderr:
logger.debug(f"Claude stderr: {stderr.decode()}")
if stdout:
logger.debug(f"Claude stdout (first 1000 chars): {stdout.decode()[:1000]}")
🤖 Prompt for AI Agents
In claude_code_api/core/claude_manager.py around lines 78 to 84, the debug
output is currently logged at non-debug levels; change the debug-level messages
(exit code, stderr and stdout debug lines) to use logger.debug() instead of
logger.error() or logger.info() so they don't pollute error logs or trigger
alerts; keep the same message text and decoding/truncation logic, only update
the logger method to logger.debug.

logger.info(
"Claude process completed",
session_id=self.session_id,
Expand Down
27 changes: 27 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
services:
claude-code-api:
build: .
container_name: claude-code-api
ports:
- "127.0.0.1:8000:8000" # Only bind to localhost since tunnel will handle external access
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
volumes:
# Mount workspace for persistent projects
- ./workspace:/home/claudeuser/app/workspace
# 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 1 to 31

Choose a reason for hiding this comment

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

medium

This docker-compose.yml is suitable for running the application, but it's not optimized for a development workflow. Any code change requires rebuilding the Docker image. To improve the development experience, consider adding a volume mount for the application source code. This allows changes to be reflected immediately without rebuilding. This is typically done in a separate docker-compose.override.yml file for development, which would be used alongside this file.

For example, you could add:

services:
  claude-code-api:
    volumes:
      - .:/home/claudeuser/app

Note that this would be most effective if the Dockerfile is updated to COPY the source code instead of git clone.