Using Jetbrains AI Pro w/ Standalone Claude Code CLI

Motivation: The cheapest Anthropic subscription for the Claude Code CLI program costs $20/mo or $200/yr. JetBrains AI Pro lets you tap into Junie by JetBrains, Claude API, Gemini, and GitHub Copilot for the affordable low price of $10/mo or $100/yr. Unfortunately, you have to use these models via JetBrains’ IDE AI Chat (the gray swirlie icon in the upper right) and it’s not as nice as the Claude Code CLI app, and I’ll be damned if I’m gonna fork over an extra $10!

Disclaimer: Not sure if this violates terms of service. Fortunately, this post is an academic exercise only.

Shell Script

TLDR; Install Claude Code CLI. Run PyCharm (or JetBrains IDE of your choice). Click the gray swirlie icon and start a chat. Run something basic like say ok to see if the proxy gateway is up and running. Then run the script provided. Sometimes you need to reboot PyCharm when claude is updated. Also note that proxy_key may be a sensitive value, so don’t share its content nor the value of the env var $ANTHROPIC_CUSTOM_HEADERS which contains the key.

  • #!/bin/bash
    
    set -eo pipefail
    
    DEBUG=0
    DRYRUN=0
    
    usage() {
      cat <<'EOF'
    Usage: ./claude-jb.sh [OPTIONS] [CLAUDE OPTIONS]
    
    Extract Claude proxy configuration from a running JetBrains IDE
    with the JetBrains AI Pro plugin, then launch Claude CLI.
    
    Wrapper options:
      --_debug      Show extracted configuration (secrets are redacted)
      --_dryrun     Extract and validate configuration, but do not launch Claude
      --_help       Show this help message
    
    All other options are passed through to Claude CLI.
    EOF
    }
    
    # Parse arguments
    # --_help, --_debug, and --_dryrun are wrapper-private options.
    # Everything else is passed through to Claude CLI.
    CLAUDE_ARGS=()
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --_help)
          usage
          exit 0
          ;;
    
        --_debug)
          DEBUG=1
          shift
          ;;
    
        --_dryrun)
          DRYRUN=1
          shift
          ;;
    
        *)
          CLAUDE_ARGS+=("$1")
          shift
          ;;
      esac
    done
    
    # Find the running Claude ACP process
    # [c]laude-acp prevents the grep command itself from appearing in the
    # results.
    PS_LINE="$(
      ps -axo command= |
        grep '[c]laude-acp' |
        grep 'proxy_key' |
        head -1 ||
        true
    )"
    
    if [[ $DEBUG -eq 1 ]]; then
      echo "Claude ACP process:"
      if [[ -n "$PS_LINE" ]]; then
        echo "$PS_LINE"
      else
        echo "<not found>"
      fi
      echo
    fi
    
    if [[ -z "$PS_LINE" ]]; then
      echo "Error: no running claude-agent-acp Claude process found." >&2
      echo "Did you start JetBrains AI chat?" >&2
      exit 1
    fi
    
    # Extract Claude proxy configuration
    ANTHROPIC_BASE_URL="$(
      printf '%s\n' "$PS_LINE" |
        sed -n 's/.*"ANTHROPIC_BASE_URL":"\([^"]*\)".*/\1/p'
    )"
    
    ANTHROPIC_AUTH_TOKEN="$(
      printf '%s\n' "$PS_LINE" |
        sed -n 's/.*"ANTHROPIC_AUTH_TOKEN":"\([^"]*\)".*/\1/p'
    )"
    
    # ANTHROPIC_CUSTOM_HEADERS, when launched by jetbrains, is a \n delimited str
    # Bad formatting will cause API failures in `claude` process
    ANTHROPIC_CUSTOM_HEADERS="$(
      printf '%s\n' "$PS_LINE" |
        sed -n 's/.*"ANTHROPIC_CUSTOM_HEADERS":"proxy_key: \([^\\]*\).*/proxy_key: \1/p'
    )"
    
    # Validate extracted configuration
    EMPTY_VARS=()
    
    [[ -z "$ANTHROPIC_BASE_URL" ]] &&
      EMPTY_VARS+=("ANTHROPIC_BASE_URL")
    
    [[ -z "$ANTHROPIC_AUTH_TOKEN" ]] &&
      EMPTY_VARS+=("ANTHROPIC_AUTH_TOKEN")
    
    [[ -z "$ANTHROPIC_CUSTOM_HEADERS" ]] &&
      EMPTY_VARS+=("ANTHROPIC_CUSTOM_HEADERS")
    
    if [[ ${#EMPTY_VARS[@]} -gt 0 ]]; then
      echo "Error: failed to extract Claude proxy configuration." >&2
      echo >&2
      echo "Missing variables:" >&2
      printf '  - %s\n' "${EMPTY_VARS[@]}" >&2
      echo >&2
    
      echo "Extracted values:" >&2
      echo "  ANTHROPIC_BASE_URL=${ANTHROPIC_BASE_URL:-<empty>}" >&2
      echo "  ANTHROPIC_AUTH_TOKEN=${ANTHROPIC_AUTH_TOKEN:-<empty>}" >&2
      echo "  ANTHROPIC_CUSTOM_HEADERS=${ANTHROPIC_CUSTOM_HEADERS:-<empty>}" >&2
    
      exit 1
    fi
    
    # Export environment
    export ANTHROPIC_BASE_URL
    export ANTHROPIC_AUTH_TOKEN
    export ANTHROPIC_CUSTOM_HEADERS
    
    # Debug printout
    if [[ $DEBUG -eq 1 ]]; then
      echo "Claude environment:"
      echo "  ANTHROPIC_BASE_URL=$ANTHROPIC_BASE_URL"
      echo "  ANTHROPIC_AUTH_TOKEN=$ANTHROPIC_AUTH_TOKEN"
      echo "  ANTHROPIC_CUSTOM_HEADERS=$ANTHROPIC_CUSTOM_HEADERS"
      echo
    fi
    
    # Dryrun exit
    if [[ $DRYRUN -eq 1 ]]; then
      echo "Dry run: Claude was not launched."
    
      # Print intended cmds
      if ((${#CLAUDE_ARGS[@]})); then
        printf 'Would have launched: claude'
        printf ' %q' "${CLAUDE_ARGS[@]}"
        printf '\n\n'
      else
        printf 'Would have launched: claude\n\n'
      fi
    
      exit 0
    fi
    
    # If debug mode, wait 5s before launching claude
    if [[ $DEBUG -eq 1 ]]; then
      # Print intended cmds
      if ((${#CLAUDE_ARGS[@]})); then
        printf 'Launching: claude'
        printf ' %q' "${CLAUDE_ARGS[@]}"
        printf '\n\n'
      else
        printf 'Launching: claude\n\n'
      fi
      # Pause for user cancellation
      echo "Claude will launch in 5s. Press Ctrl-C to cancel."
      for ((i=5; i>0; i--)); do
        printf '\rLaunching Claude in %d... ' "$i"
        sleep 1
      done
    
      printf '\rLaunching Claude now!       \n'
    fi
    
    # Launch Claude
    claude "${CLAUDE_ARGS[@]}"
    
    

Details

Using ps aux and grep, find PyCharm’s (or your JetBrains IDE of choice’s) PID. Then run pstree on the PID.

The tree shows that:

flowchart LR
    subgraph PY[PyCharm process]
        SERVER[Proxy server<br/>http://localhost:xyz]
    end

    NPM[npm exec<br/>claude-agent-acp@]
    ACP[node<br/>claude-agent-acp]
    C[claude<br/>args + env vars]

    PY --> NPM
    NPM --> ACP
    ACP --> C
    C -->|POST <br/>/v1/messages | SERVER

pycharm -> npm exec […]/claude-agent-acp@ -> node [...]/claude-agent-acp -> [...]/claude [... a bunch of args and env vars ...]

We can confirm that the node process is talking to the claude process by examining the lsof data:

# List of open unix sockets on the `node [...]/claude-agent-acp` process
$ lsof -a -p node-claude-agent-acp-pid -U
COMMAND PID                       USER      FD  TYPE DEVICE              SIZE/OFF        NODE NAME
node    node-claude-agent-acp-pid myuser   14u  unix 0x705868ea096fdb20  0t0             ->0xa47584b733d77022
node    node-claude-agent-acp-pid myuser   16u  unix 0x1ce54ae6345e3cc2  0t0             ->0x91e277bc456a4c8a
node    node-claude-agent-acp-pid myuser   18u  unix 0xeac6a0dcec0bcb6   0t0             ->0x96c22ec3bb9f349f

# List of open unix sockets on claude-pid, the claude process
$ lsof -a -p claude-pid -U
COMMAND   PID      USER      FD  TYPE DEVICE                  SIZE/OFF                NODE NAME
claude  claude-pid myuser    0u  unix 0xa47584b733d77022      0t0                    ->0x705868ea096fdb20  # <--- matches 14u above
claude  claude-pid myuser    1u  unix 0x91e277bc456a4c8a      0t0                    ->0x1ce54ae6345e3cc2  # <--- matches 16u above
claude  claude-pid myuser    2u  unix 0x96c22ec3bb9f349f      0t0                    ->0xeac6a0dcec0bcb6   # <--- matches 18u above
claude  claude-pid myuser    4u  unix 0x96c22ec3bb9f349f      0t0                    ->0xeac6a0dcec0bcb6
claude  claude-pid myuser    6u  unix 0xb9b36545e1a2a943      0t0                    /tmp/cc-socks/claude-pid.sock
claude  claude-pid myuser   10u  unix 0x91e277bc456a4c8a      0t0                    ->0x1ce54ae6345e3cc2
claude  claude-pid myuser   11u  unix 0x6ff505b1f1b842f8      0t0                    ->0x6dd2d38f0896218b
claude  claude-pid myuser   12u  unix 0x82405c4c33e81557      0t0                    ->0x22b997a5cee2937a
claude  claude-pid myuser   14u  unix 0x7b12c1c0419685df      0t0                    ->0x779c603d3952fb2
claude  claude-pid myuser   18u  unix 0xe988823ea80273f9      0t0                    ->0xaf978a6161bb7c7d

# List of shared sockets between node and claude
$ lsof -nP | grep -E 'COMMAND|0xa47584b733d77022|0x91e277bc456a4c8a|0x96c22ec3bb9f349f|0x705868ea096fdb20|0x1ce54ae6345e3cc2|0xeac6a0dcec0bcb6'
COMMAND     PID      USER   FD      TYPE             DEVICE    SIZE/OFF                NODE NAME
node      43185 dburnwood   14u     unix 0x705868ea096fdb20         0t0                     ->0xa47584b733d77022
node      43185 dburnwood   16u     unix 0x1ce54ae6345e3cc2         0t0                     ->0x91e277bc456a4c8a
node      43185 dburnwood   18u     unix  0xeac6a0dcec0bcb6         0t0                     ->0x96c22ec3bb9f349f
claude    43186 dburnwood    0u     unix 0xa47584b733d77022         0t0                     ->0x705868ea096fdb20
claude    43186 dburnwood    1u     unix 0x91e277bc456a4c8a         0t0                     ->0x1ce54ae6345e3cc2
claude    43186 dburnwood    2u     unix 0x96c22ec3bb9f349f         0t0                     ->0xeac6a0dcec0bcb6
claude    43186 dburnwood    4u     unix 0x96c22ec3bb9f349f         0t0                     ->0xeac6a0dcec0bcb6
claude    43186 dburnwood   10u     unix 0x91e277bc456a4c8a         0t0                     ->0x1ce54ae6345e3cc2:w

Let’s examine the claude command:

$ ps eww -p claude-pid # This command will print out the entire claude command including env vars

The arg ANTHROPIC_BASE_URL indicates a server running on localhost:<some_port>. Running lsof -nP -iTCP:<some_port> -sTCP:LISTEN shows that it belongs to PyCharm.

Use tcpdump -i lo0 -A -s 0 tcp port <some_port>, then in PyCharm’s AI chat, run something basic, like say ok. You should be able to intercept a POST request to /v1/messages/:

POST /v1/messages?beta=true HTTP/1.1
Accept: application/json
Authorization: Bearer acp-proxy
Content-Type: application/json
User-Agent: claude-cli/2.1.232 (external, sdk-ts, agent-sdk/0.3.232)
X-Claude-Code-Session-Id: code-session-id
X-IntelliJ-Proxy-Agent-ID: acp.registry.claude-acp
X-IntelliJ-Proxy-Client-Execution-ID: some-proxy-client-exec-id
X-Stainless-Arch: arm64
X-Stainless-Lang: js
X-Stainless-OS: MacOS
X-Stainless-Package-Version: 0.112.1
X-Stainless-Retry-Count: 0
X-Stainless-Runtime: node
X-Stainless-Runtime-Version: v26.3.0
X-Stainless-Timeout: 600
anthropic-beta: interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,claude-code-20250219,advisor-tool-2026-03-01
anthropic-dangerous-direct-browser-access: true
anthropic-version: 2023-06-01
proxy_key: your-proxy-key
x-app: cli
Connection: keep-alive
Host: 127.0.0.1:<some_port>
Accept-Encoding: gzip, deflate, br, zstd
Content-Length: 123

[... a bunch of json data ...]

I find you don’t seem to need to include all the X-headers. You’ll also notice that claude’s ANTHROPIC_CUSTOM_HEADERS is a multi-line string, and misformatting it will cause the server to error out. The only value you really need is proxy_key. Here’s a minimal curl command that works:

$ curl -X POST http://127.0.0.1:<some_port>/v1/messages -H 'content-type: application/json' -H "proxy_key: your-proxy-key-here" -H 'Authorization: Bearer acp-proxy' --data '{"model":"claude-sonnet-4-5","max_tokens":5,"messages":[{"role":"user","content":"Say OK"}]}'
{"model":"your-model","id":"msg_some_id","type":"message","role":"assistant","content":[{"type":"text","text":"OK"}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":9,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard","inference_geo":"not_available"}}

I couldn’t find any other interesting endpoints.