Skip to content
Open
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
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

# Recommended output token configuration for larger tool outputs
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000
CLAUDE_MODEL=claude-sonnet-4-5-20250929
# Optional model override specifically for Haiku
# CLAUDE_HAIKU_MODEL=claude-haiku-4-5-20251001

# =============================================================================
# OPTION 1: Direct Anthropic (default, no router)
Expand All @@ -12,6 +15,10 @@ ANTHROPIC_API_KEY=your-api-key-here
# OR use OAuth token instead
# CLAUDE_CODE_OAUTH_TOKEN=your-oauth-token-here

# Optional: Route Claude API calls through a custom proxy endpoint
# CLAUDE_API_BASE_URL=https://your-proxy.example.com
# CLAUDE_API_AUTH_TOKEN=your-proxy-auth-token

# =============================================================================
# OPTION 2: Router Mode (use alternative providers)
# =============================================================================
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,22 @@ cd shannon
# Option A: Export environment variables
export ANTHROPIC_API_KEY="your-api-key" # or CLAUDE_CODE_OAUTH_TOKEN
export CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 # recommended
export CLAUDE_MODEL="claude-sonnet-4-5-20250929" # optional override
# export CLAUDE_HAIKU_MODEL="claude-haiku-4-5-20251001" # optional Haiku-specific override
# Optional: route Claude calls through your own proxy
# export CLAUDE_API_BASE_URL="https://your-proxy.example.com"
# export CLAUDE_API_AUTH_TOKEN="your-proxy-auth-token"

# Option B: Create a .env file
cat > .env << 'EOF'
ANTHROPIC_API_KEY=your-api-key
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000
CLAUDE_MODEL=claude-sonnet-4-5-20250929
# Optional:
# CLAUDE_HAIKU_MODEL=claude-haiku-4-5-20251001
# Optional:
# CLAUDE_API_BASE_URL=https://your-proxy.example.com
# CLAUDE_API_AUTH_TOKEN=your-proxy-auth-token
EOF

# 3. Run a pentest
Expand Down Expand Up @@ -297,6 +308,21 @@ rules:

If your application uses two-factor authentication, simply add the TOTP secret to your config file. The AI will automatically generate the required codes during testing.

### Custom Claude Proxy Endpoint

You can route Claude SDK traffic through a custom proxy without enabling Router Mode.

```bash
CLAUDE_API_BASE_URL=https://your-proxy.example.com
# Optional if your proxy requires token auth
CLAUDE_API_AUTH_TOKEN=your-proxy-auth-token
```

Environment variable precedence:

- `CLAUDE_API_BASE_URL` overrides `ANTHROPIC_BASE_URL`
- `CLAUDE_API_AUTH_TOKEN` overrides `ANTHROPIC_AUTH_TOKEN`

### [EXPERIMENTAL - UNSUPPORTED] Router Mode (Alternative Providers)

Shannon can experimentally route requests through alternative AI providers using claude-code-router. This mode is not officially supported and is intended primarily for:
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ services:
environment:
- TEMPORAL_ADDRESS=temporal:7233
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
- CLAUDE_API_BASE_URL=${CLAUDE_API_BASE_URL:-} # Optional: custom proxy endpoint for Claude API
- CLAUDE_API_AUTH_TOKEN=${CLAUDE_API_AUTH_TOKEN:-} # Optional: auth token for custom proxy
- ANTHROPIC_BASE_URL=${ANTHROPIC_BASE_URL:-} # Optional: route through claude-code-router
- ANTHROPIC_AUTH_TOKEN=${ANTHROPIC_AUTH_TOKEN:-} # Auth token for router
- ROUTER_DEFAULT=${ROUTER_DEFAULT:-} # Model name when using router (e.g., "gemini,gemini-2.5-pro")
- CLAUDE_MODEL=${CLAUDE_MODEL:-claude-sonnet-4-5-20250929}
- CLAUDE_HAIKU_MODEL=${CLAUDE_HAIKU_MODEL:-}
- CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN:-}
- CLAUDE_CODE_MAX_OUTPUT_TOKENS=${CLAUDE_CODE_MAX_OUTPUT_TOKENS:-64000}
depends_on:
Expand Down
55 changes: 36 additions & 19 deletions shannon
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,34 @@ case "$OSTYPE" in
msys*) export MSYS_NO_PATHCONV=1 ;;
esac

# Detect Podman vs Docker and set compose files accordingly
# Podman doesn't support host-gateway, so we only include the Docker override for actual Docker
COMPOSE_BASE="docker-compose.yml"
if command -v podman &>/dev/null; then
# Podman detected (either native or via Docker Desktop shim) - use base config only
COMPOSE_OVERRIDE=""
# Detect a working compose command and compose files.
# Prefer Docker Compose v2, then docker-compose, then podman compose.
COMPOSE_CMD=()
COMPOSE_ENGINE=""
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=(docker compose)
COMPOSE_ENGINE="docker"
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=(docker-compose)
COMPOSE_ENGINE="docker"
elif podman compose version >/dev/null 2>&1; then
COMPOSE_CMD=(podman compose)
COMPOSE_ENGINE="podman"
else
# Docker detected - include extra_hosts override for Linux localhost access
COMPOSE_OVERRIDE="-f docker-compose.docker.yml"
echo "ERROR: No supported compose command found."
echo "Install Docker Compose (docker compose / docker-compose) or Podman Compose."
exit 1
fi
COMPOSE_FILE="$COMPOSE_BASE"

COMPOSE_FILES=(-f docker-compose.yml)
# Podman doesn't support host-gateway; only include Docker override when using Docker.
if [ "$COMPOSE_ENGINE" = "docker" ]; then
COMPOSE_FILES+=(-f docker-compose.docker.yml)
fi

compose() {
"${COMPOSE_CMD[@]}" "${COMPOSE_FILES[@]}" "$@"
}

# Load .env if present
if [ -f .env ]; then
Expand Down Expand Up @@ -90,7 +107,7 @@ parse_args() {

# Check if Temporal is running and healthy
is_temporal_ready() {
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE exec -T temporal \
compose exec -T temporal \
temporal operator cluster health --address localhost:7233 2>/dev/null | grep -q "SERVING"
}

Expand All @@ -100,7 +117,7 @@ ensure_containers() {
# Docker compose will only recreate if the mount actually changed
if [ -n "$OUTPUT_DIR" ]; then
echo "Ensuring worker has correct output mount..."
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE up -d worker 2>/dev/null || true
compose up -d worker 2>/dev/null || true
fi

# Quick check: if Temporal is already healthy, we're good
Expand All @@ -113,9 +130,9 @@ ensure_containers() {
if [ "$REBUILD" = "true" ]; then
# Force rebuild without cache (use when code changes aren't being picked up)
echo "Rebuilding with --no-cache..."
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE build --no-cache worker
compose build --no-cache worker
fi
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE up -d --build
compose up -d --build

# Wait for Temporal to be ready
echo "Waiting for Temporal to be ready..."
Expand Down Expand Up @@ -184,7 +201,7 @@ cmd_start() {
# Handle ROUTER flag - start claude-code-router for multi-model support
if [ "$ROUTER" = "true" ]; then
# Check if router is already running
if docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE --profile router ps router 2>/dev/null | grep -q "running"; then
if compose --profile router ps router 2>/dev/null | grep -q "running"; then
echo "Router already running, skipping startup..."
else
echo "Starting claude-code-router..."
Expand All @@ -195,7 +212,7 @@ cmd_start() {
fi

# Start router with profile
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE --profile router up -d router
compose --profile router up -d router

# Give router a few seconds to start (health check disabled for now - TODO: debug later)
echo "Waiting for router to start..."
Expand Down Expand Up @@ -235,7 +252,7 @@ cmd_start() {
[ -n "$WORKSPACE" ] && ARGS="$ARGS --workspace $WORKSPACE"

# Run the client to submit workflow
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE exec -T worker \
compose exec -T worker \
node dist/temporal/client.js "$URL" "$CONTAINER_REPO" $ARGS
}

Expand Down Expand Up @@ -299,17 +316,17 @@ cmd_workspaces() {
# Ensure containers are running (need worker to execute node)
ensure_containers

docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE exec -T worker \
compose exec -T worker \
node dist/temporal/workspaces.js
}

cmd_stop() {
parse_args "$@"

if [ "$CLEAN" = "true" ]; then
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE --profile router down -v
compose --profile router down -v
else
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE --profile router down
compose --profile router down
fi
}

Expand Down
14 changes: 13 additions & 1 deletion src/ai/claude-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,21 @@ export async function runClaudePrompt(
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
sdkEnv.CLAUDE_CODE_OAUTH_TOKEN = process.env.CLAUDE_CODE_OAUTH_TOKEN;
}
const anthropicBaseUrl = process.env.CLAUDE_API_BASE_URL || process.env.ANTHROPIC_BASE_URL;
if (anthropicBaseUrl) {
sdkEnv.ANTHROPIC_BASE_URL = anthropicBaseUrl;
}
const anthropicAuthToken = process.env.CLAUDE_API_AUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN;
if (anthropicAuthToken) {
sdkEnv.ANTHROPIC_AUTH_TOKEN = anthropicAuthToken;
}
const claudeModel =
process.env.CLAUDE_MODEL ||
process.env.CLAUDE_HAIKU_MODEL ||
'claude-sonnet-4-5-20250929';

const options = {
model: 'claude-sonnet-4-5-20250929',
model: claudeModel,
maxTurns: 10_000,
cwd: sourceDir,
permissionMode: 'bypassPermissions' as const,
Expand Down