Running codex cli in VS Code devcontainer

Resolved 💬 3 comments Opened Aug 19, 2025 by alpaka Closed Sep 8, 2025

What is the type of issue?

Documentation is missing

Edit: I finally got it to work, see the end of the issue. Documentation is still missing, however ;)

What is the issue?

Environment
I'm running Claude Code in a VS Code devcontainer with a custom Dockerfile. I wanted to try the Codex CLI in the same project. Codex works fine if I run it on the host.

OS: Windows 11 with WSL2

Setup steps

  • added RUN npm i -g @openai/codex to my Dockerfile
  • logged in locally
  • mounted auth.json into the container using "source=${localEnv:HOME}/.codex/auth.json,target=/etc/codex/auth.json,readonly,type=bind"
  • added cp /etc/codex/auth.json /home/node/.codex/auth.json to the postCreateCommand
  • added api.openai.com to the URL whitelist
  • run codex inside container, verify /status prints as expected
  • curl https://api.openai.com returns as expected
  • disabling the firewall temporarily, codex runs normally

Issue
codex is "thinking" forever when I try any prompt. Pressing ctrl+c after 15 minutes and checking /status afterwards displays token usage as 0. This seems to be a networking issue, but there is no log or error information available

If I understand the problem correctly, codex is trying to access IP addresses not listed when using dig +short A api.openai.com, either because it's trying to access another domain or because api.openai.com changes IPs (some cloudflare thing?)

Resolving api.openai.com...
Adding 162.159.140.245 for api.openai.com
Adding 172.66.0.243 for api.openai.com

This is the output of netstat -np --inet | grep "codex"
tcp 0 1 172.17.0.2:38806 172.64.155.209:443 SYN_SENT 1089/codex-x86_64-u
later
tcp 0 1 172.17.0.2:47382 104.18.32.47:443 SYN_SENT 1089/codex-x86_64-u

I don't understand why this is not a problem when using run_in_container.sh where you use the same dig command I use in init_firewall.sh

_Please provide documentation in README.md about which URLs have to be whitelisted and how you recommend to run codex inside a container_

Additional information
Status output

  • Path: /workspace
  • Approval Mode: on-request
  • Sandbox: workspace-write
  • Session ID: 841645eb-c10b-4dff-85b7-bdc55a021763

👤 Account
  • Signed in with ChatGPT
  • Login:
  • Plan: Plus

🧠 Model
  • Name: gpt-5
  • Provider: OpenAI
  • Reasoning Effort: Medium
  • Reasoning Summaries: Auto```

curl https://api.openai.com
{
"message": "Welcome to the OpenAI API! Documentation is available at https://platform.openai.com/docs/api-reference"
}%
̀ ``

excerpt from

# Resolve and add other allowed domains
for domain in \
    "api.openai.com" \
    "googlehosted.l.googleusercontent.com" \
    "registry.npmjs.org" \
    "api.anthropic.com" \
    "sentry.io" \
    "statsig.anthropic.com" \
    "statsig.com"; do
    echo "Resolving $domain..."
    ips=$(dig +short A "$domain")
    if [ -z "$ips" ]; then
        echo "ERROR: Failed to resolve $domain"
        exit 1
    fi
    
    while read -r ip; do
        if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
            echo "ERROR: Invalid IP from DNS for $domain: $ip"
            exit 1
        fi
        echo "Adding $ip for $domain"
        ipset add allowed-domains "$ip"
    done < <(echo "$ips")
done

Edit:
I figured it out. After having GPT-5 rewrite the script to add cloudflare IPs to the whitelist, codex works in the container now. Here's my full init-firewall.sh for reference

#!/bin/bash
set -euo pipefail  # Exit on error, undefined vars, and pipeline failures
IFS=$'\n\t'       # Stricter word splitting

# 1. Extract Docker DNS info BEFORE any flushing
DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11" || true)

# Flush existing rules and delete existing ipsets
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X
ipset destroy allowed-domains 2>/dev/null || true

# 2. Selectively restore ONLY internal Docker DNS resolution
if [ -n "$DOCKER_DNS_RULES" ]; then
    echo "Restoring Docker DNS rules..."
    iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true
    iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true
    echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat
else
    echo "No Docker DNS rules to restore"
fi

# First allow DNS and localhost before any restrictions
# Allow outbound DNS
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
# Allow inbound DNS responses
iptables -A INPUT -p udp --sport 53 -j ACCEPT
# Allow outbound SSH
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
# Allow inbound SSH responses
iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
# Allow localhost
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

# Create ipset with CIDR support (IPv4)
ipset create allowed-domains hash:net

# --- GitHub IPv4 ranges ---
echo "Fetching GitHub IP ranges..."
gh_json=$(curl -s https://api.github.com/meta)
if [ -z "$gh_json" ]; then
    echo "ERROR: Failed to fetch GitHub IP ranges"
    exit 1
fi
if ! echo "$gh_json" | jq -e '.web and .api and .git' >/dev/null; then
    echo "ERROR: GitHub API response missing required fields"
    exit 1
fi
echo "Processing GitHub IPs..."
echo "$gh_json" \
  | jq -r '(.web + .api + .git)[]' \
  | aggregate -q \
  | while read -r cidr; do
      if [[ ! "$cidr" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$ ]]; then
        echo "ERROR: Invalid CIDR range from GitHub meta: $cidr"
        exit 1
      fi
      echo "Adding GitHub range $cidr"
      ipset add -! allowed-domains "$cidr"
    done

# --- Cloudflare IPv4 ranges ---
echo "Fetching Cloudflare IP ranges..."
cf_json=$(curl -s https://api.cloudflare.com/client/v4/ips)
if [ -z "$cf_json" ]; then
    echo "ERROR: Failed to fetch Cloudflare IP ranges"
    exit 1
fi
if ! echo "$cf_json" | jq -e '.result.ipv4_cidrs' >/dev/null; then
    echo "ERROR: Cloudflare API response missing ipv4_cidrs"
    exit 1
fi
echo "Processing Cloudflare IPv4 CIDRs..."
echo "$cf_json" \
  | jq -r '.result.ipv4_cidrs[]' \
  | aggregate -q \
  | while read -r cidr; do
      if [[ ! "$cidr" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$ ]]; then
        echo "ERROR: Invalid CIDR range from Cloudflare: $cidr"
        exit 1
      fi
      echo "Adding Cloudflare range $cidr"
      ipset add -! allowed-domains "$cidr"   # -! ignores duplicates
    done
# Note: IPv6 ranges are available from Cloudflare but not added here since this script configures iptables (IPv4).
# Add a separate inet6 ipset + ip6tables if you need IPv6.

# Resolve and add other allowed domains (A records only)
for domain in \
    "api.openai.com" \
    "platform.openai.com" \
    "googlehosted.l.googleusercontent.com" \
    "registry.npmjs.org" \
    "api.anthropic.com" \
    "sentry.io" \
    "statsig.anthropic.com" \
    "statsig.com"
do
    echo "Resolving $domain..."
    ips=$(dig +short A "$domain" | grep -E '^[0-9]{1,3}(\.[0-9]{1,3}){3}$' | sort -u)
    if [ -z "$ips" ]; then
        echo "ERROR: Failed to resolve $domain"
        exit 1
    fi
    while read -r ip; do
        echo "Adding $ip for $domain"
        ipset add -! allowed-domains "$ip"
    done <<< "$ips"
done

# Get host IP from default route
HOST_IP=$(ip route | awk '/^default/ {print $3; exit}')
if [ -z "$HOST_IP" ]; then
    echo "ERROR: Failed to detect host IP"
    exit 1
fi
HOST_NETWORK=$(echo "$HOST_IP" | sed 's/\.[0-9]*$/.0\/24/')
echo "Host network detected as: $HOST_NETWORK"

# Set up remaining iptables rules
iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT
iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT

# Set default policies to DROP first
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP

# First allow established connections for already approved traffic
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Then allow only specific outbound traffic to allowed domains
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT

echo "Firewall configuration complete"
echo "Verifying firewall rules..."
if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then
    echo "ERROR: Firewall verification failed - was able to reach https://example.com"
    exit 1
else
    echo "Firewall verification passed - unable to reach https://example.com as expected"
fi

# Verify GitHub API access
if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then
    echo "ERROR: Firewall verification failed - unable to reach https://api.github.com"
    exit 1
else
    echo "Firewall verification passed - able to reach https://api.github.com as expected"
fi

# Verify Anthropic API access
if ! curl --connect-timeout 5 https://api.anthropic.com >/dev/null 2>&1; then
    echo "ERROR: Firewall verification failed - unable to reach https://api.anthropic.com"
    exit 1
else
    echo "Firewall verification passed - able to reach https://api.anthropic.com as expected"
fi

# Verify OpenAI API access
if ! curl --connect-timeout 5 https://api.openai.com >/dev/null 2>&1; then
    echo "ERROR: Firewall verification failed - unable to reach https://api.openai.com/"
    exit 1
else
    echo "Firewall verification passed - able to reach https://api.openai.com/ as expected"
fi

# Cloudflare reachability probe
if ! curl --connect-timeout 5 https://www.cloudflare.com/cdn-cgi/trace >/dev/null 2>&1; then
    echo "WARN: Could not reach Cloudflare trace endpoint; check ranges and DNS."
else
    echo "Cloudflare verification passed."
fi

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗