Skip to content

Conversation

@christag
Copy link

@christag christag commented Oct 2, 2025

User description

I created a Dockerfile and compose file, as well as updated the type to text/event-stream for use with OpenWebUI.
You can set an ANTHROPIC_API_KEY in environement OR leave the max sub setting on and then youll just have to exec into the container to authenticate once.

One issue right now is being able to authenticate MCP servers from within claude code since localhost redirect doesnt work nicely.


PR Type

Enhancement


Description

  • Added Docker containerization with Dockerfile and compose

  • Fixed streaming response media type to text/event-stream

  • Enhanced Claude process debugging and error logging

  • Added environment variable support for API configuration


Diagram Walkthrough

flowchart LR
  A["Docker Setup"] --> B["Claude Code API"]
  B --> C["Streaming Response"]
  B --> D["Environment Config"]
  C --> E["text/event-stream"]
  D --> F["ANTHROPIC_API_KEY"]
Loading

File Walkthrough

Relevant files
Enhancement
Dockerfile
Complete Docker containerization setup                                     

Dockerfile

  • Creates Ubuntu-based container with Node.js and Python
  • Installs Claude Code CLI globally as root user
  • Sets up non-root user with workspace and config directories
  • Configures entrypoint script for API key or Claude Max authentication
+103/-0 
claude_manager.py
Enhanced Claude process debugging and environment support

claude_code_api/core/claude_manager.py

  • Added environment variable copying to subprocess execution
  • Enhanced debug logging for exit codes and stderr/stdout
  • Improved error tracking and process monitoring
+9/-2     
Configuration changes
docker-compose.yml
Docker Compose service configuration                                         

docker-compose.yml

  • Defines service configuration for claude-code-api container
  • Maps port 8000 to localhost with volume mounts
  • Sets environment variables for Claude Max usage
  • Includes health check and restart policy
+27/-0   
Bug fix
chat.py
Fix streaming and JSON response media types                           

claude_code_api/api/chat.py

  • Changed streaming response media type from text/plain to
    text/event-stream
  • Updated non-streaming response to use JSONResponse with
    application/json
+2/-2     

Summary by CodeRabbit

  • New Features

    • Containerized deployment support (Dockerfile + compose service) and a host-run OAuth proxy for callbacks.
  • Bug Fixes

    • Full, multi-message streaming restored (no artificial chunk limit or early termination); SSE streaming uses text/event-stream. Non-streaming responses now aggregate complete assistant output and return proper JSON.
  • Documentation

    • New guides for Claude integration, Docker OAuth setup, and a combined fixes/test storyboard.
  • Chores

    • .gitignore updated to ignore fix_env.patch; improved process environment handling and debug logging.

@coderabbitai
Copy link

coderabbitai bot commented Oct 2, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds containerization and orchestration (Dockerfile, docker-compose), an OAuth callback proxy, docs (CLAUDE.md, DOCKER_OAUTH_SETUP.md, FIXES_SUMMARY.md), streaming and non‑stream response handling changes, environment propagation and debug logging for Claude subprocesses, and updates .gitignore to ignore fix_env.patch.

Changes

Cohort / File(s) Summary
Container image & compose
Dockerfile, docker-compose.yml
Adds Dockerfile to build a non‑root runtime with Node.js and Claude CLI, entrypoint script and healthcheck; adds docker-compose.yml service claude-code-api with localhost port bindings, env vars, volumes, restart policy and healthcheck.
OAuth proxy & docs
oauth-proxy.py, DOCKER_OAUTH_SETUP.md
Adds an OAuth callback proxy server (oauth-proxy.py) with /oauth/register, /oauth/callback, /health; documents host‑proxy setup and container OAuth flow in DOCKER_OAUTH_SETUP.md.
Docs / Release notes
CLAUDE.md, FIXES_SUMMARY.md
New comprehensive developer and fixes documentation describing architecture, streaming fixes, OAuth proxy usage, testing and deployment steps.
Streaming & Chat endpoint
claude_code_api/utils/streaming.py, claude_code_api/api/chat.py
Removes artificial chunk/early‑termination limits; streams until real end, aggregates multiple assistant messages with separators for non‑stream responses; streaming media_type set to text/event-stream; non‑stream returns JSONResponse with application/json.
Process management / env
claude_code_api/core/claude_manager.py
Passes current environment into subprocess (env=os.environ.copy()); adds debug logging of process exit code, stdout and stderr before error handling.
Repository hygiene
.gitignore
Adds fix_env.patch to .gitignore, retains existing test script note.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant API as Chat Endpoint
  participant CM as Claude Manager
  participant P as Claude Process
  C->>API: POST /v1/chat/completions (payload, stream flag)
  API->>CM: request process (payload, env propagated)
  CM->>P: spawn Claude CLI (env=os.environ.copy())
  P-->>CM: stdout SSE / JSON chunks
  CM-->>API: parsed messages (streamed chunks or final messages)
  alt stream = true
    Note right of API #D0F0C0: SSE media_type: text/event-stream
    API-->>C: SSE events (data chunks until None)
  else
    Note right of API #F0E68C: JSONResponse application/json
    API-->>C: final JSONResponse (aggregated assistant messages joined with `---`)
  end
  CM->>CM: log exit code, stdout, stderr
Loading
sequenceDiagram
  autonumber
  participant Dev as Developer
  participant Proxy as OAuth Proxy
  participant Cont as Container (API)
  Dev->>Proxy: POST /oauth/register (session_id, callback_port)
  Proxy-->>Dev: { callback_url -> http://host:port/oauth/callback?state=... }
  Dev->>Browser: open callback_url (user auth)
  Browser->>Proxy: GET /oauth/callback?state=...&code=...
  Proxy->>Cont: POST http://container_host:callback_port/oauth/callback (forwarded)
  Cont-->>Proxy: 200 OK
  Proxy-->>Browser: success HTML
  Proxy->>Proxy: track active_sessions
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I nibbled through the Docker hay,
Forwarded keys and routes today.
Streams now flow until they cease,
Env and logs brought tidy peace.
A rabbit's thump — deploy with glee! 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly identifies the two central changes—adding a Dockerfile and modifying the streaming response logic—which accurately reflects the main focus of the pull request. It directly references real aspects of the diff without introducing unrelated or generic terms.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-merge-for-open-source
Copy link

qodo-merge-for-open-source bot commented Oct 2, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Supply chain risk

Description: The Docker build clones the repository source directly from a public Git URL at build
time, risking supply-chain integrity issues and producing images that may not reflect the
intended commit; prefer copying local, reviewed sources into the image or pinning a
commit/tag and verifying integrity.
Dockerfile [49-55]

Referred Code
RUN git clone https://github.com/christag/claude-code-api.git .

# 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 .
Excessive privileges

Description: Granting passwordless sudo to the non-root user broadens privileges inside the container
and can escalate impact if the container is compromised; consider least-privilege and
avoid NOPASSWD sudo unless strictly necessary.
Dockerfile [33-34]

Referred Code
echo "claudeuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
Secret persistence

Description: The entrypoint writes the ANTHROPIC_API_KEY into a config file inside the container
filesystem, which can leak via image layers, logs, or volume mounts; prefer runtime
environment usage or secret stores without persisting secrets to disk.
Dockerfile [71-101]

Referred Code
# 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\
  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\


 ... (clipped 10 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
No custom compliance provided

Follow the guide to enable custom compliance check.

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@gemini-code-assist
Copy link

Summary of Changes

Hello @christag, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the claude-code-api by introducing full Docker containerization, making it easier to deploy and run the application. It also refines the API's streaming capabilities to align with modern event-stream standards, improving integration with front-end applications. Additionally, the changes include better debugging tools for the Claude process and more flexible API key management through environment variables.

Highlights

  • Docker Containerization: Introduced a Dockerfile and docker-compose.yml to containerize the claude-code-api, simplifying deployment and management. The Docker setup includes an Ubuntu base, Node.js, Python, the Claude Code CLI, and a non-root user configuration.
  • Streaming Response Update: Modified the streaming API response media type from 'text/plain' to 'text/event-stream' to ensure compatibility with tools like OpenWebUI and improve real-time data delivery.
  • Enhanced Debugging and Environment Handling: Improved the Claude process management by adding environment variable copying to subprocess execution and incorporating detailed debug logging for exit codes, stderr, and stdout, aiding in troubleshooting.
  • API Key and Authentication Flexibility: The Docker setup now supports configuring the ANTHROPIC_API_KEY via environment variables or using Claude Max subscription authentication, with clear instructions for each method.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@qodo-merge-for-open-source
Copy link

qodo-merge-for-open-source bot commented Oct 2, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Security
Remove unnecessary sudo privileges

In the Dockerfile, remove the line that grants passwordless sudo access to the
claudeuser to improve container security.

Dockerfile [31-33]

 # Create non-root user for running Claude Code
-RUN useradd -m -s /bin/bash claudeuser && \
-    echo "claudeuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
+RUN useradd -m -s /bin/bash claudeuser
  • Apply / Chat
Suggestion importance[1-10]: 10

__

Why: The suggestion correctly identifies and resolves a critical security vulnerability by removing unnecessary passwordless sudo privileges, adhering to the principle of least privilege.

High
High-level
Refactor Dockerfile to build from source

Modify the Dockerfile to use COPY to add the source code from the local build
context into the image, instead of cloning the repository from GitHub. This
change ensures reproducible builds and follows standard containerization
practices.

Examples:

Dockerfile [49]
RUN git clone https://github.com/christag/claude-code-api.git .

Solution Walkthrough:

Before:

# Dockerfile

...
WORKDIR /home/claudeuser/app

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

# Install dependencies
RUN pip3 install --user -e .
...

After:

# Dockerfile

...
WORKDIR /home/claudeuser/app

# Copy application source code
COPY --chown=claudeuser:claudeuser . .

# Install dependencies
RUN pip3 install --user -e .
...
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical flaw in the Dockerfile where git clone is used instead of COPY, making builds non-reproducible and preventing local changes from being included in the image.

High
Possible issue
Safely generate JSON configuration file

In the Dockerfile's entrypoint script, replace the cat heredoc with a jq command
to safely generate the config.json file, preventing issues with special
characters in the ANTHROPIC_API_KEY.

Dockerfile [79-84]

-cat > ~/.config/claude/config.json << EOF
-{
-  "apiKey": "$ANTHROPIC_API_KEY",
-  "autoUpdate": false
-}
-EOF
+jq -n \
+  --arg key "$ANTHROPIC_API_KEY" \
+  '{apiKey: $key, autoUpdate: false}' > ~/.config/claude/config.json

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a potential runtime failure due to unescaped characters in an environment variable and proposes a robust fix using jq.

Medium
General
Use appropriate log level for debugging

Change the log level for debug output from logger.error and logger.info to
logger.debug to ensure logs are categorized correctly.

claude_code_api/core/claude_manager.py [79-84]

 # Debug logging MUST be here, before any error checking
-logger.error(f"=== DEBUG: Exit code: {self.process.returncode} ===")
+logger.debug(f"=== DEBUG: Exit code: {self.process.returncode} ===")
 if stderr:
-    logger.error(f"=== DEBUG: Claude stderr: {stderr.decode()} ===")
+    logger.debug(f"=== DEBUG: Claude stderr: {stderr.decode()} ===")
 if stdout:
-    logger.info(f"=== DEBUG: Claude stdout: {stdout.decode()[:1000]} ===")
+    logger.debug(f"=== DEBUG: Claude stdout: {stdout.decode()[:1000]} ===")
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly points out the misuse of logger.error for debug information and suggests using logger.debug, which improves log clarity and maintainability.

Low
  • Update

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces Docker support for the application via a Dockerfile and docker-compose.yml, which is a great enhancement for deployment and development consistency. It also includes a fix for the streaming response media type and improves debugging logs. My review focuses on improving the Docker setup for security, best practices, and maintainability. I've identified a critical issue with how the source code is added to the image, a high-severity security concern with user permissions, and several medium-severity suggestions to improve the Dockerfile, logging, and development workflow with Docker Compose.

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 . .

Comment on lines +32 to +33
RUN useradd -m -s /bin/bash claudeuser && \
echo "claudeuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

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 +23 to +25
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
apt-get install -y nodejs && \
node --version && npm --version

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

Comment on lines +72 to +100
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\
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

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.

# 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.

Comment on lines +80 to +84
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]} ===")

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 1 to 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 No newline at end of file

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
claude_code_api/core/claude_manager.py (1)

72-73: Consider filtering sensitive environment variables.

Passing os.environ.copy() propagates all environment variables to the Claude subprocess, including potentially sensitive keys like ANTHROPIC_API_KEY. If the Claude process logs or exposes its environment, this could leak secrets.

Consider filtering the environment to pass only necessary variables:

+            # Filter environment to only pass necessary variables
+            safe_env = {
+                k: v for k, v in os.environ.items()
+                if not k.startswith(('SECRET_', 'PASSWORD_', 'TOKEN_'))
+                or k in ('ANTHROPIC_API_KEY',)  # Explicitly allow needed keys
+            }
+
             self.process = await asyncio.create_subprocess_exec(
                 *cmd,
                 cwd=src_dir,
                 stdout=asyncio.subprocess.PIPE,
                 stderr=asyncio.subprocess.PIPE,
-                env=os.environ.copy()
+                env=safe_env
             )
claude_code_api/api/chat.py (1)

281-281: Redundant media_type parameter.

JSONResponse already sets media_type="application/json" by default. Explicitly passing it is redundant but harmless.

You can simplify to:

-            return JSONResponse(content=response, media_type="application/json")
+            return JSONResponse(content=response)
Dockerfile (1)

23-25: Pin Node.js version for reproducibility.

Using setup_18.x installs the latest Node.js 18.x version, which can change over time and affect build reproducibility.

Pin to a specific Node.js version:

-RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
+# Pin to specific Node.js version for reproducibility
+RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
     apt-get install -y nodejs && \
+    npm install -g npm@9.8.1 && \
     node --version && npm --version

Also consider pinning the @anthropic-ai/claude-code package version on line 28.

docker-compose.yml (1)

7-13: Remove unused CLAUDE_PROJECT_ROOT
The CLAUDE_PROJECT_ROOT variable is declared in docker-compose.yml but not referenced anywhere in the codebase. Remove it to avoid confusion.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd618dc and 2c24b21.

📒 Files selected for processing (5)
  • .gitignore (1 hunks)
  • Dockerfile (1 hunks)
  • claude_code_api/api/chat.py (2 hunks)
  • claude_code_api/core/claude_manager.py (1 hunks)
  • docker-compose.yml (1 hunks)
🔇 Additional comments (6)
.gitignore (1)

331-332: LGTM!

Adding fix_env.patch to .gitignore is appropriate for preventing accidental commits of environment-specific patches or temporary fixes.

claude_code_api/api/chat.py (1)

207-207: LGTM! Correct MIME type for SSE.

Changing from text/plain to text/event-stream aligns with the Server-Sent Events (SSE) standard and matches OpenAI's streaming API format. This improves compatibility with clients like OpenWebUI.

Dockerfile (2)

68-69: LGTM! Appropriate healthcheck configuration.

The healthcheck configuration is well-designed with reasonable intervals, timeout, retries, and start period. This will help orchestration systems detect when the API is ready and healthy.


76-85: Verify API key configuration logic.

The entrypoint script only configures the API key if ANTHROPIC_API_KEY is set AND USE_CLAUDE_MAX is not true. However, if neither condition is met, the API may fail to authenticate with Claude Code at runtime.

Consider adding validation to fail fast if authentication is not properly configured:

 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\
+  echo "ERROR: No authentication configured. Set ANTHROPIC_API_KEY or USE_CLAUDE_MAX=true"\n\
+  exit 1\n\
 fi\n\

This ensures the container fails immediately if misconfigured rather than starting and failing later.

docker-compose.yml (2)

5-6: LGTM! Security-conscious port binding.

Binding to 127.0.0.1:8000 instead of 0.0.0.0:8000 ensures the API is only accessible from localhost, which is appropriate when using an external tunnel for access. This prevents unintended exposure of the API.


22-27: LGTM! Appropriate healthcheck configuration.

The healthcheck configuration matches the Dockerfile and provides proper health monitoring for container orchestration. The start period allows sufficient time for the API to initialize.

Comment on lines +78 to +84
# 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]} ===")
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.

Comment on lines +32 to +33
RUN useradd -m -s /bin/bash claudeuser && \
echo "claudeuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
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.

WORKDIR /home/claudeuser/app

# Clone claude-code-api
RUN git clone https://github.com/christag/claude-code-api.git .
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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c24b21 and e0e3e2f.

📒 Files selected for processing (7)
  • CLAUDE.md (1 hunks)
  • DOCKER_OAUTH_SETUP.md (1 hunks)
  • FIXES_SUMMARY.md (1 hunks)
  • claude_code_api/api/chat.py (3 hunks)
  • claude_code_api/utils/streaming.py (4 hunks)
  • docker-compose.yml (1 hunks)
  • oauth-proxy.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • docker-compose.yml
🧰 Additional context used
🧬 Code graph analysis (2)
claude_code_api/utils/streaming.py (1)
claude_code_api/core/claude_manager.py (1)
  • get_output (136-163)
claude_code_api/api/chat.py (1)
claude_code_api/core/claude_manager.py (1)
  • get_output (136-163)
🪛 markdownlint-cli2 (0.18.1)
FIXES_SUMMARY.md

170-170: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

CLAUDE.md

41-41: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

DOCKER_OAUTH_SETUP.md

47-47: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


108-108: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.13.2)
oauth-proxy.py

72-72: Local variable response_text is assigned to but never used

Remove assignment to unused variable response_text

(F841)


84-84: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


122-122: Do not catch blind exception: Exception

(BLE001)


123-123: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


126-126: Unused method argument: request

(ARG002)


271-271: Unused function argument: request

(ARG001)


359-359: Possible binding to all interfaces

(S104)

Comment on lines +53 to +76
session_id = query_params.get('state', 'unknown')
callback_port = query_params.get('callback_port')

if not callback_port:
# Try to determine callback port from state or other params
logger.warning("No callback_port specified, using default container port")
callback_port = self.container_port
else:
callback_port = int(callback_port)

# Forward to container
target_url = f"http://{self.container_host}:{callback_port}/oauth/callback"

logger.info(f"Forwarding OAuth callback to: {target_url}")

async with ClientSession() as session:
try:
# Forward the callback with all query parameters
async with session.get(target_url, params=query_params, timeout=10) as resp:
response_text = await resp.text()

logger.info(f"Container response: {resp.status}")

# Return success page to user
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

Use registered callback ports when forwarding OAuth callbacks

handle_register_callback records session→port mappings, but handle_oauth_callback ignores that data whenever the provider redirect lacks an explicit callback_port query param. In the normal OAuth flow the provider only echoes state=...; as a result every callback is forwarded to the default container port (8000), so session-specific callback listeners never receive the code, breaking authentication. Please resolve the port via active_sessions when the query omits callback_port (and keep the default as a final fallback).

-            callback_port = query_params.get('callback_port')
-
-            if not callback_port:
-                # Try to determine callback port from state or other params
-                logger.warning("No callback_port specified, using default container port")
-                callback_port = self.container_port
-            else:
-                callback_port = int(callback_port)
+            callback_port_param = query_params.get('callback_port')
+
+            if callback_port_param is not None:
+                try:
+                    callback_port = int(callback_port_param)
+                except ValueError:
+                    logger.error("Invalid callback_port '%s', falling back to default", callback_port_param)
+                    callback_port = self.container_port
+            else:
+                callback_port = self.active_sessions.get(session_id, self.container_port)
+                if session_id in self.active_sessions:
+                    logger.info(
+                        "Resolved callback port %s from registered session %s",
+                        callback_port,
+                        session_id,
+                    )
+                else:
+                    logger.warning(
+                        "No callback_port provided and session %s not registered; using default %s",
+                        session_id,
+                        self.container_port,
+                    )
📝 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
session_id = query_params.get('state', 'unknown')
callback_port = query_params.get('callback_port')
if not callback_port:
# Try to determine callback port from state or other params
logger.warning("No callback_port specified, using default container port")
callback_port = self.container_port
else:
callback_port = int(callback_port)
# Forward to container
target_url = f"http://{self.container_host}:{callback_port}/oauth/callback"
logger.info(f"Forwarding OAuth callback to: {target_url}")
async with ClientSession() as session:
try:
# Forward the callback with all query parameters
async with session.get(target_url, params=query_params, timeout=10) as resp:
response_text = await resp.text()
logger.info(f"Container response: {resp.status}")
# Return success page to user
session_id = query_params.get('state', 'unknown')
callback_port_param = query_params.get('callback_port')
if callback_port_param is not None:
try:
callback_port = int(callback_port_param)
except ValueError:
logger.error("Invalid callback_port '%s', falling back to default", callback_port_param)
callback_port = self.container_port
else:
callback_port = self.active_sessions.get(session_id, self.container_port)
if session_id in self.active_sessions:
logger.info(
"Resolved callback port %s from registered session %s",
callback_port,
session_id,
)
else:
logger.warning(
"No callback_port provided and session %s not registered; using default %s",
session_id,
self.container_port,
)
# Forward to container
target_url = f"http://{self.container_host}:{callback_port}/oauth/callback"
logger.info(f"Forwarding OAuth callback to: {target_url}")
async with ClientSession() as session:
try:
# Forward the callback with all query parameters
async with session.get(target_url, params=query_params, timeout=10) as resp:
response_text = await resp.text()
logger.info(f"Container response: {resp.status}")
# Return success page to user
🧰 Tools
🪛 Ruff (0.13.2)

72-72: Local variable response_text is assigned to but never used

Remove assignment to unused variable response_text

(F841)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (7)
CLAUDE.md (1)

1-151: Excellent comprehensive documentation for the project.

This documentation provides clear architectural guidance, setup instructions, and debugging tips. It accurately reflects the codebase structure and will be valuable for developers working with or extending the project.

Optional: Address the static analysis hint.

Line 41 uses bold text for a heading. Consider using a proper markdown heading instead:

-**ClaudeProcess (claude_manager.py:18-228)**
+#### ClaudeProcess (claude_manager.py:18-228)

This applies to other bold "headings" at lines 28, 34, 42, and 47 as well.

oauth-proxy.py (4)

68-89: Address unused variable and improve error logging.

The code has minor issues flagged by static analysis:

  1. Line 72: response_text is assigned but never used
  2. Line 84: Should use logging.exception for exception context

Apply these improvements:

                 try:
                     # Forward the callback with all query parameters
                     async with session.get(target_url, params=query_params, timeout=10) as resp:
-                        response_text = await resp.text()
-
                         logger.info(f"Container response: {resp.status}")
 
                         # Return success page to user
                         return web.Response(
                             text=self._success_html(session_id),
                             content_type='text/html',
                             status=200
                         )
 
                 except ClientError as e:
-                    logger.error(f"Failed to forward to container: {e}")
+                    logger.exception(f"Failed to forward to container: {e}")
                     return web.Response(
                         text=self._error_html(str(e)),
                         content_type='text/html',
                         status=502
                     )

99-124: Improve error handling specificity.

Lines 122-123 catch bare Exception and should use logging.exception for better debugging context.

Apply this improvement:

         except Exception as e:
-            logger.error(f"Error registering callback: {e}")
+            logger.exception("Error registering callback")
             return web.json_response({'error': str(e)}, status=500)

126-134: Remove unused request parameter.

The request parameter in handle_health is unused (line 126).

Either use it for logging or mark it as unused:

-    async def handle_health(self, request: web.Request) -> web.Response:
+    async def handle_health(self, _request: web.Request) -> web.Response:
         """Health check endpoint."""

271-316: Remove unused request parameter in handle_root.

Line 271's request parameter is unused.

Mark as unused:

-    async def handle_root(request):
+    async def handle_root(_request):
         return web.Response(
FIXES_SUMMARY.md (1)

1-406: Comprehensive documentation of fixes and testing procedures.

This document accurately describes the streaming improvements and OAuth proxy implementation, provides clear testing procedures, and includes helpful troubleshooting guidance. It aligns well with the code changes in the PR.

Optional: Add language specifier to code block.

Line 170 has a fenced code block without a language specifier (flagged by markdownlint):

-```
+```text
 Starting OAuth Proxy on port 8888
 Forwarding to container at localhost:8000
 OAuth callback URL: http://localhost:8888/oauth/callback
 Press Ctrl+C to stop

</blockquote></details>
<details>
<summary>DOCKER_OAUTH_SETUP.md (1)</summary><blockquote>

`1-303`: **Excellent OAuth proxy setup documentation.**

This guide provides clear, step-by-step instructions for setting up the OAuth proxy, including troubleshooting, advanced configuration, and production deployment guidance. It accurately reflects the oauth-proxy.py implementation and will help users successfully configure MCP server authentication in Docker.



**Optional: Add language specifiers to code blocks.**

Lines 47 and 108 have fenced code blocks without language specifiers (flagged by markdownlint):

```diff
# Line 47
-```
+```text
 Starting OAuth Proxy on port 8888
 Forwarding to container at localhost:8000
 OAuth callback URL: http://localhost:8888/oauth/callback
 Press Ctrl+C to stop

Line 108

- +text
┌─────────────────┐
│ Your Browser │
│ (on host) │
...

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c24b21 and e0e3e2f.

📒 Files selected for processing (7)
  • CLAUDE.md (1 hunks)
  • DOCKER_OAUTH_SETUP.md (1 hunks)
  • FIXES_SUMMARY.md (1 hunks)
  • claude_code_api/api/chat.py (3 hunks)
  • claude_code_api/utils/streaming.py (4 hunks)
  • docker-compose.yml (1 hunks)
  • oauth-proxy.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • docker-compose.yml
🧰 Additional context used
🧬 Code graph analysis (2)
claude_code_api/api/chat.py (1)
claude_code_api/core/claude_manager.py (1)
  • get_output (136-163)
claude_code_api/utils/streaming.py (1)
claude_code_api/core/claude_manager.py (1)
  • get_output (136-163)
🪛 markdownlint-cli2 (0.18.1)
DOCKER_OAUTH_SETUP.md

47-47: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


108-108: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

FIXES_SUMMARY.md

170-170: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

CLAUDE.md

41-41: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🪛 Ruff (0.13.2)
oauth-proxy.py

72-72: Local variable response_text is assigned to but never used

Remove assignment to unused variable response_text

(F841)


84-84: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


122-122: Do not catch blind exception: Exception

(BLE001)


123-123: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


126-126: Unused method argument: request

(ARG002)


271-271: Unused function argument: request

(ARG001)


359-359: Possible binding to all interfaces

(S104)

🔇 Additional comments (7)
claude_code_api/utils/streaming.py (2)

87-131: LGTM! Streaming improvements enable complete agentic workflows.

The removal of artificial limits (max_chunks = 5) and early termination logic allows streaming to capture all assistant responses and tool execution results. The changes correctly rely on the None end signal from get_output() rather than arbitrary cutoffs.


346-405: Verify separator choice for multi-message aggregation.

The aggregation logic correctly collects all assistant messages and uses \n\n---\n\n as a separator when multiple messages exist. This is a reasonable choice for readability.

Consider whether the separator pattern might conflict with actual content. For example, if assistant messages naturally contain --- markdown separators, this could create ambiguity. You may want to:

  1. Use a more distinctive separator (e.g., \n\n=== Response {n} ===\n\n)
  2. Document the separator choice for API consumers

Run this to check if existing content uses similar patterns:

claude_code_api/api/chat.py (3)

207-207: Correct media type for Server-Sent Events.

The change from "text/plain" to "text/event-stream" is the proper media type for SSE streaming responses, ensuring compatibility with SSE clients and the OpenAI SDK.


219-235: LGTM! Message collection now captures complete workflows.

Removing the artificial safety limit and early termination allows the endpoint to collect all messages until get_output() naturally completes. This aligns with the streaming improvements and ensures complete agentic responses.


276-276: Explicit JSONResponse improves consistency.

Returning JSONResponse(content=response, media_type="application/json") makes the response type explicit and ensures the correct Content-Type header, improving consistency with the streaming path's explicit media type.

oauth-proxy.py (2)

358-359: Binding to all interfaces requires careful consideration.

Line 359 binds to 0.0.0.0, exposing the OAuth proxy to all network interfaces. This is flagged by static analysis (S104) as a potential security risk.

For a localhost-only OAuth proxy, binding to 0.0.0.0 is unnecessary and increases attack surface. Consider:

  1. If localhost-only is intended: Change to 127.0.0.1
  2. If remote access is needed: Document the security implications and recommend firewall rules

Current code:

web.run_app(app, host='0.0.0.0', port=args.port)

Safer default for local development:

web.run_app(app, host='127.0.0.1', port=args.port)

Or make it configurable:

parser.add_argument('--host', default='127.0.0.1', help='Host to bind to')
# ...
web.run_app(app, host=args.host, port=args.port)

Based on the PR description ("localhost redirect does not work cleanly"), binding to 127.0.0.1 seems more appropriate.


38-62: Verify callback_port extraction and default behavior.

The logic defaults to self.container_port when callback_port is missing from query params. However, line 54 extracts callback_port from query params, which may not be the intended source for dynamic port forwarding.

The PR description mentions MCP servers redirect to localhost:[random-port], but this implementation:

  1. Extracts callback_port from query params (line 54)
  2. Falls back to the fixed container_port (line 59)

This means the MCP server must include callback_port in its OAuth redirect URL. Verify this matches the actual OAuth flow:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant