Skip to content

Instantly share code, notes, and snippets.

@realgenekim
Last active October 31, 2025 19:02
Show Gist options
  • Select an option

  • Save realgenekim/aed6cb81ee8206dcea8b59b6db523763 to your computer and use it in GitHub Desktop.

Select an option

Save realgenekim/aed6cb81ee8206dcea8b59b6db523763 to your computer and use it in GitHub Desktop.
Fix Maven/Gradle 401 errors in Claude Code web environment
# Java proxy configuration for local proxy shim
# Source this file before running Maven/Gradle/Clojure builds
#
# Usage: . ./.proxy-java-shim.env && mvn test
export JAVA_TOOL_OPTIONS="-Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=15080 -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=15080"
export MAVEN_OPTS="$JAVA_TOOL_OPTIONS"

🔧 Claude Code Web: Fix Maven/Gradle Dependencies (401 Unauthorized)

Problem: You want to run Clojure programs in Claude Code Web, but you're getting all sorts of errors when trying to download dependencies from Maven Central or Clojars. Maven/Gradle fails with 401 Unauthorized in Claude Code web environment

Solution: Local proxy shim that handles authentication translation

Result: ✅ All JVM builds work perfectly

My Commentary: I was in utter awe watching Claude Code Web figure this out, along with lots of hints from ChatGPT Pro.) Together, they’re like the world’s best sysadmin and networking engineer, who also happens to also know Maven Central and Clojars.

🚀 Quick Start (3 Steps)

1. Create the proxy shim

Save proxy_shim.py (from this gist) and make it executable:

mkdir -p script
# Save proxy_shim.py to script/proxy_shim.py
chmod +x script/proxy_shim.py

2. Create configuration files

# Save .proxy-java-shim.env to project root
# Save settings.xml to ~/.m2/settings.xml

3. Run your build

# Terminal 1: Start proxy shim
python3 script/proxy_shim.py

# Terminal 2: Run your build
. ./.proxy-java-shim.env && clojure -M:test    # Clojure
. ./.proxy-java-shim.env && mvn test           # Maven
. ./.proxy-java-shim.env && gradle test        # Gradle

📦 Files in This Gist

File Purpose
README.md This file - quick start guide
proxy_shim.py The proxy shim (Python 3, no dependencies)
.proxy-java-shim.env Java configuration
settings.xml Maven configuration
Makefile.snippet Optional automation

🎯 What This Solves

Environment: Claude Code on the web (https://claude.ai/code) Symptom:

Could not transfer artifact org.clojure:clojure:pom:1.12.0
status code: 401, reason phrase: Unauthorized (401)

Cause: Claude Code's security proxy returns 401 instead of RFC 9110 compliant 407, breaking Java's HTTP client authentication.

Fix: Local proxy shim translates authentication:

Java → localhost:15080 (no auth) → Shim → Claude Proxy (JWT) → Maven Central

✅ Verification

After setup, test with:

# 1. Start shim in background
python3 script/proxy_shim.py &

# 2. Test dependency download
. ./.proxy-java-shim.env
clojure -Sdeps '{:deps {org.clojure/data.json {:mvn/version "2.5.0"}}}' -e '(+ 1 1)'

# Expected: Downloads successfully

🔄 Automated Usage (Optional)

Use the Makefile.snippet to automate shim lifecycle:

make proxy-shim-start    # Start shim
make test                # Run tests
make proxy-shim-stop     # Stop shim

# OR all-in-one:
make test-with-shim

🧠 Technical Details

Why This Works

Claude Code's proxy violates RFC 9110 by returning 401 (origin auth) instead of 407 (proxy auth). Java's HTTP client doesn't send proxy credentials when it sees 401.

The shim sends both headers:

  • Proxy-Authorization: Basic <creds> (correct per RFC)
  • Authorization: Basic <creds> (what the proxy expects)

Security

✅ Listens on localhost only (127.0.0.1) ✅ Credentials never logged ✅ Uses standard environment variables ✅ No new security exposure (Claude Code already has credentials)

Features

  • Pure Python 3 - No dependencies
  • Async I/O - Handles CONNECT tunnels correctly
  • Production Ready - Tested with 97 tests, 448 assertions
  • Works with: Clojure, Maven, Gradle, SBT, Leiningen

📊 Results

Before:

❌ status code: 401, reason phrase: Unauthorized (401)

After:

✅ Downloading: org/clojure/data.json/2.5.0/data.json-2.5.0.pom from central
✅ 97 tests, 448 assertions, 0 failures

🐛 Troubleshooting

Shim won't start: Verify $HTTPS_PROXY is set (Claude Code sets this automatically)

Still getting 401: Check Maven settings point to 127.0.0.1:15080

Shim crashes: Check /tmp/proxy-shim.log for errors

📚 Resources

📝 Tested With

  • ✅ Clojure (tools.deps)
  • ✅ Maven
  • ✅ Gradle
  • ✅ Multiple concurrent connections
  • ✅ HTTPS CONNECT tunnels

Copy all files from this gist and follow Quick Start above. Should work in < 5 minutes!

# Gradle proxy configuration for Claude Code web environment
# Save to: gradle.properties (in project root or ~/.gradle/)
#
# This configures Gradle to use the local proxy shim at 127.0.0.1:15080
systemProp.http.proxyHost=127.0.0.1
systemProp.http.proxyPort=15080
systemProp.https.proxyHost=127.0.0.1
systemProp.https.proxyPort=15080
systemProp.http.nonProxyHosts=localhost
# Makefile snippet for automating proxy shim lifecycle
# Add these targets to your Makefile for convenient shim management
#
# Usage:
# make proxy-shim-start # Start shim in background
# make proxy-shim-stop # Stop shim
# make test-with-shim # All-in-one: start, test, stop
# Start proxy shim in background
.PHONY: proxy-shim-start
proxy-shim-start:
@if [ -f .proxy-shim.pid ] && kill -0 $$(cat .proxy-shim.pid) 2>/dev/null; then \
echo "Proxy shim already running (pid $$(cat .proxy-shim.pid))"; \
else \
PROXY_SHIM_LISTEN=127.0.0.1:15080 nohup python3 script/proxy_shim.py \
>/tmp/proxy-shim.log 2>&1 & \
echo $$! > .proxy-shim.pid; \
sleep 1; \
echo "✓ Proxy shim started on 127.0.0.1:15080 (pid $$(cat .proxy-shim.pid))"; \
echo " Log: /tmp/proxy-shim.log"; \
fi
# Stop proxy shim
.PHONY: proxy-shim-stop
proxy-shim-stop:
@if [ -f .proxy-shim.pid ]; then \
if kill $$(cat .proxy-shim.pid) 2>/dev/null; then \
echo "✓ Proxy shim stopped (pid $$(cat .proxy-shim.pid))"; \
else \
echo "⚠ Proxy shim process not found (pid $$(cat .proxy-shim.pid))"; \
fi; \
rm -f .proxy-shim.pid; \
else \
echo "No proxy shim PID file found"; \
fi
# Run tests with proxy shim (all-in-one: start shim, run tests, stop shim)
.PHONY: test-with-shim
test-with-shim: proxy-shim-start
@echo "Running tests through proxy shim..."
@. ./.proxy-java-shim.env 2>/dev/null || true; \
$(MAKE) test; \
EXIT_CODE=$$?; \
$(MAKE) proxy-shim-stop; \
exit $$EXIT_CODE
# Example test target (replace with your actual test command)
.PHONY: test
test:
# Replace with your actual test command:
# clojure -M:test # Clojure
# mvn test # Maven
# gradle test # Gradle
@echo "Running tests..."
@echo "TODO: Add your test command here"
#!/usr/bin/env python3
import asyncio, base64, os, sys
from urllib.parse import urlparse, unquote
UP = os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY")
if not UP:
print("ERROR: Set HTTPS_PROXY or HTTP_PROXY with credentials (http://user:pass@host:port).", file=sys.stderr); sys.exit(1)
p = urlparse(UP)
if not (p.hostname and p.port and p.username and p.password):
print("ERROR: Proxy URL must include host, port, username, and password.", file=sys.stderr); sys.exit(1)
UP_HOST, UP_PORT = p.hostname, p.port
UP_AUTH = base64.b64encode(f"{unquote(p.username)}:{unquote(p.password)}".encode()).decode()
LISTEN = os.environ.get("PROXY_SHIM_LISTEN", "127.0.0.1:15080")
LHOST, LPORT = LISTEN.split(":")[0], int(LISTEN.split(":")[1])
HDR_END = b"\r\n\r\n"
CRLF = b"\r\n"
async def pipe(src, dst):
try:
while True:
data = await src.read(65536)
if not data: break
dst.write(data); await dst.drain()
except Exception:
pass
finally:
try: dst.write_eof()
except Exception: pass
def inject_header(block: bytes, name: bytes, value: bytes) -> bytes:
# Insert header after request line
head, sep, rest = block.partition(CRLF)
return head + CRLF + name + b": " + value + CRLF + rest
async def handle(reader, writer):
try:
# Read client request head (start-line + headers)
data = b""
while HDR_END not in data and len(data) < 65536:
chunk = await reader.read(4096)
if not chunk: break
data += chunk
if not data: writer.close(); await writer.wait_closed(); return
# Parse request line
line, _, headers = data.partition(CRLF)
parts = line.decode(errors="ignore").split()
method = parts[0] if parts else ""
# CONNECT handling (HTTPS tunneling)
if method == "CONNECT":
target = parts[1] if len(parts) > 1 else ""
# Open upstream proxy
upr, upw = await asyncio.open_connection(UP_HOST, UP_PORT)
# Send CONNECT with both Proxy-Authorization and Authorization to placate 401-style proxies.
# (For CONNECT, these headers are consumed by the proxy and not forwarded to origin.)
req = (f"CONNECT {target} HTTP/1.1\r\n"
f"Host: {target}\r\n"
f"Proxy-Authorization: Basic {UP_AUTH}\r\n"
f"Authorization: Basic {UP_AUTH}\r\n"
f"Proxy-Connection: keep-alive\r\n\r\n").encode()
upw.write(req); await upw.drain()
# Read upstream response head
up_resp = b""
while HDR_END not in up_resp and len(up_resp) < 65536:
chunk = await upr.read(4096)
if not chunk: break
up_resp += chunk
status = up_resp.split(b"\r\n",1)[0] if up_resp else b""
ok = status.startswith(b"HTTP/1.1 200") or status.startswith(b"HTTP/1.0 200")
if not ok:
# surface upstream response to client for debugging
writer.write(up_resp or b"HTTP/1.1 502 Bad Gateway\r\n\r\n"); await writer.drain()
writer.close(); upw.close();
try: await upw.wait_closed(); await writer.wait_closed()
except Exception: pass
return
# Tell client the tunnel is up
writer.write(b"HTTP/1.1 200 Connection Established\r\n\r\n"); await writer.drain()
# Now tunnel bytes both ways
await asyncio.gather(pipe(reader, upw), pipe(upr, writer))
upw.close(); writer.close()
try: await upw.wait_closed(); await writer.wait_closed()
except Exception: pass
return
# Plain HTTP over proxy (Java sends absolute-form request line)
# Ensure Proxy-Authorization is present; DO NOT add Authorization here to avoid leaking creds to origin.
if b"\nProxy-Authorization:" not in headers:
data = inject_header(data, b"Proxy-Authorization", f"Basic {UP_AUTH}".encode())
# Open upstream proxy and forward the modified head plus any buffered tail
upr, upw = await asyncio.open_connection(UP_HOST, UP_PORT)
upw.write(data); await upw.drain()
# Stream remaining body (if any) and responses
await asyncio.gather(pipe(reader, upw), pipe(upr, writer))
upw.close(); writer.close()
try: await upw.wait_closed(); await writer.wait_closed()
except Exception: pass
except Exception as e:
print(f"[ERROR] {e}", file=sys.stderr)
try: writer.close(); await writer.wait_closed()
except Exception: pass
async def main():
srv = await asyncio.start_server(handle, LHOST, LPORT)
print(f"[proxy-shim] listening on {LHOST}:{LPORT} → upstream {UP_HOST}:{UP_PORT} (creds from HTTPS_PROXY/HTTP_PROXY)")
async with srv: await srv.serve_forever()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass
<?xml version="1.0" encoding="UTF-8"?>
<!--
Maven settings for Claude Code web environment
Save to: ~/.m2/settings.xml
This configures Maven to use the local proxy shim at 127.0.0.1:15080
which handles authentication to Claude Code's security proxy.
-->
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
<proxies>
<!-- HTTP proxy configuration -->
<proxy>
<id>local-shim</id>
<active>true</active>
<protocol>http</protocol>
<host>127.0.0.1</host>
<port>15080</port>
<nonProxyHosts>localhost</nonProxyHosts>
</proxy>
<!-- HTTPS proxy configuration -->
<proxy>
<id>local-shim-https</id>
<active>true</active>
<protocol>https</protocol>
<host>127.0.0.1</host>
<port>15080</port>
<nonProxyHosts>localhost</nonProxyHosts>
</proxy>
</proxies>
</settings>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment