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