Fixing Gradle on Claude Code Web with a PreToolUse Hook

Claude Code on the Web runs inside a sandboxed environment that routes all traffic through an HTTP proxy.
Tools like curl pick up the https_proxy environment variable automatically - but Java doesn't.
The Gradle wrapper uses java.net.HttpURLConnection under the hood, which completely ignores https_proxy.
The result? java.net.UnknownHostException: services.gradle.org every single time.
This is a known issue that affects every Java/Kotlin project on Claude Code Web. I wrote a Claude Code Hook that fixes it.
The Problem
When you run ./gradlew in a Claude Code Web session, Java tries to resolve services.gradle.org directly - bypassing the sandbox proxy entirely. You get this:
Exception in thread "main" java.net.UnknownHostException: services.gradle.org
at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:567)
...
at org.gradle.wrapper.GradleWrapperMain.main(SourceFile:67)
The frustrating part: curl downloads the exact same URL just fine because it honors https_proxy. The sandbox network works, Java just doesn't know about it.
The Fix: A PreToolUse Hook
Claude Code Hooks let you run shell scripts before or after specific tool invocations. We can use a PreToolUse hook on the Bash tool to configure the proxy before any command runs.
Let's first look at the full hook script:
#!/bin/bash
set -euo pipefail
# Only run in remote (Claude Code on the web) environments
if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
exit 0
fi
# Configure Gradle proxy so Java can resolve external hosts
if [ -n "${https_proxy:-}" ]; then
PROXY_HOST=$(echo "$https_proxy" | sed 's|.*@||' | sed 's|:.*||')
PROXY_PORT=$(echo "$https_proxy" | sed 's|.*:||')
PROXY_USER=$(echo "$https_proxy" | sed 's|http://||' | sed 's|@.*||' | sed 's|:.*||')
PROXY_PASS=$(echo "$https_proxy" | sed 's|http://||' | sed 's|@.*||' | sed 's|^[^:]*:||')
mkdir -p ~/.gradle
cat > ~/.gradle/gradle.properties << PROPEOF
systemProp.http.proxyHost=$PROXY_HOST
systemProp.http.proxyPort=$PROXY_PORT
systemProp.http.proxyUser=$PROXY_USER
systemProp.http.proxyPassword=$PROXY_PASS
systemProp.https.proxyHost=$PROXY_HOST
systemProp.https.proxyPort=$PROXY_PORT
systemProp.https.proxyUser=$PROXY_USER
systemProp.https.proxyPassword=$PROXY_PASS
systemProp.jdk.http.auth.tunneling.disabledSchemes=
systemProp.jdk.http.auth.proxying.disabledSchemes=
PROPEOF
fi
# Download Gradle distribution if not already cached
cd "$CLAUDE_PROJECT_DIR"
./gradlew --version > /dev/null 2>&1
# Warm Gradle dependency cache
./gradlew compileKotlin compileTestKotlin --no-daemon 2>/dev/null || true
In detail:
CLAUDE_CODE_REMOTE-> environment variable that's only set in Claude Code Web sessions, so the hook is a no-op locally- We parse the
https_proxyURL (format:http://user:pass@host:port) into its components usingsed ~/.gradle/gradle.properties-> where Gradle reads JVM proxy system properties fromsystemProp.http.proxyHost/systemProp.https.proxyHost-> tells Java's HTTP client to route through the sandbox proxyjdk.http.auth.tunneling.disabledSchemes=/jdk.http.auth.proxying.disabledSchemes=-> by default Java disables Basic auth for HTTPS proxies, we clear that restriction so proxy authentication works./gradlew --version-> triggers the Gradle distribution download through the now-configured proxy./gradlew compileKotlin compileTestKotlin-> warms the dependency cache so Claude doesn't wait for downloads mid-task
Setting Up the Hook
Add this to your project's .claude/settings.json:
// .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/bin/bash .claude/hooks/pre-tool-use.sh"
}
]
}
]
}
}
Then place the script at .claude/hooks/pre-tool-use.sh in your project.
CLAUDE_CODE_REMOTE first and exits immediately in local environments, there's zero overhead when running Claude Code on your own machine.Why a Hook Instead of a CLAUDE.md Instruction?
You could write "always configure the Gradle proxy before running builds" in your CLAUDE.md, but:
- Claude might forget or skip it
- It wastes tokens re-explaining the setup every session
- A hook is deterministic it runs every time, no LLM reasoning required
Hooks are the right tool when you need guaranteed, repeatable setup. Save CLAUDE.md for guidance that requires judgment.
The Result
With this hook in place, Claude Code Web sessions with Spring Boot / Kotlin projects just work. No more UnknownHostException, no more manually downloading Gradle distributions with curl, no more wasted context window explaining proxy configuration to the AI.
If you want to learn more about Claude Code check out my other post on how I used Claude Code to plan a complex security migration. My side business PhotoQuest is also built with Spring Boot + htmx and was the project where I first ran into this issue.
Questions? Ping me on twitter.com/tschuehly.



