April 26, 2026 โ€ข Version: 2026.2.22-2

OpenAI OAuth Login Requires TTY: Non-Interactive Authentication Failure

The `openclaw models auth login` command fails in non-interactive environments due to mandatory TTY detection, blocking companion apps and scripts from automating OpenAI Codex OAuth flow.

๐Ÿ” Symptoms

Primary Manifestation

When attempting to invoke the OpenAI OAuth login from a non-interactive context (CI/CD pipelines, companion applications, remote shells), the command terminates immediately without opening the browser authorization flow:

$ openclaw models auth login --provider openai-codex
Error: This command requires an interactive terminal (TTY).
Run this command directly in your terminal to continue.

Alternatively, use: openclaw onboard --auth-choice openai-codex
$ echo $?
1

Full Onboarding Workaround Produces 7-Step Wizard

Attempting the suggested workaround navigates through all configuration screens:

$ openclaw onboard --auth-choice openai-codex

Welcome to OpenClaw Setup (Step 1/7: QuickStart)
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
โ€บ โ— Run QuickStart Setup
  โ—‹ Use Existing Configuration
  โ—‹ Manual Configuration
Select: [Enter to continue]

The wizard sequences through:

  1. QuickStart โ€” Initial setup choice
  2. Use Existing โ€” Select existing or create new config
  3. Channel Selection โ€” Skip channel setup
  4. Skills Installation โ€” Decline skills
  5. Hooks Configuration โ€” Skip webhook setup
  6. Agent Hatching โ€” Decline hatching
  7. Authorization โ€” Finally reaches OAuth flow

Environment Detection Failure

The TTY detection occurs in src/auth/oauth-detect.ts:

$ node -e "console.log('isTTY:', process.stdin.isTTY)"
isTTY: undefined
$ node -e "console.log('isTTY:', !!process.stdout.isTTY)"  
isTTY: false

๐Ÿง  Root Cause

Architectural Design Decision

The OpenAI OAuth authentication flow was designed with a security constraint: OAuth browser redirects require human confirmation to prevent automated token exfiltration. The original implementation assumed CLI usage only, embedding TTY detection as a gatekeeper mechanism in src/cli/auth-commands.ts:

// Line 23-31 of auth-commands.ts
function requireInteractive(): void {
  if (!process.stdin.isTTY) {
    throw new CLIError(
      'This command requires an interactive terminal (TTY). ' +
      'Run this command directly in your terminal to continue.'
    );
  }
}

OAuth Flow Execution Path

The authentication sequence follows this internal chain:

  1. openclaw models auth login --provider openai-codex invoked
  2. requireInteractive() check executes
  3. If TTY absent โ†’ immediate exit with error
  4. If TTY present โ†’ OAuthFlowManager.start() called
  5. Browser opened via openai-auth://authorize?... custom protocol
  6. Polling loop waits for token via localhost:8765/callback
  7. Token stored to ~/.openclaw/credentials/openai-codex.json

Why Onboarding Redirect Also Fails

The –auth-choice openai-codex flag was added post-v1.0 but maps to a legacy redirect handler that still validates interactive mode at the onboarding coordinator level:

// src/onboard/coordinator.ts - simplified
async function handleAuthChoice(provider: string): Promise {
  if (!process.stdin.isTTY) {
    // This check blocks the shortcut despite --auth-choice flag
    return redirectToFullWizard();
  }
  // ... direct OAuth routing logic never reached
}

Configuration Storage Target

Successfully authenticated tokens are persisted to:

~/.openclaw/
โ””โ”€โ”€ credentials/
    โ””โ”€โ”€ openai-codex.json    # Contains encrypted refresh_token, expires_at

๐Ÿ› ๏ธ Step-by-Step Fix

Solution A: Non-Interactive OAuth Command (Recommended)

Modify the authentication command to support headless operation by introducing a –no-interactive flag that skips TTY validation but retains browser redirect:

# Before (fails in non-interactive environments)
openclaw models auth login --provider openai-codex

# After (supports headless operation)
openclaw models auth login --provider openai-codex --no-interactive

Implementation in src/cli/auth-commands.ts:

// Modify the command definition (lines 12-18)
program
  .command('models auth login')
  .description('Authenticate with a model provider via OAuth')
  .requiredOption('--provider <provider>', 'Provider name (e.g., openai-codex)')
  .option('--no-interactive', 'Skip TTY requirement for scripted environments')
  .option('--callback-port <port>', 'Callback server port', '8765')
  .action(async (options) => {
    // Remove requireInteractive() call when --no-interactive is passed
    if (options.interactive) {
      requireInteractive();
    }
    await OAuthFlowManager.start({
      provider: options.provider,
      callbackPort: parseInt(options.callbackPort, 10),
      headless: !options.interactive
    });
  });

Solution B: Environment Variable Override

For companion applications, set the OPENCLAW_NO_TTY_CHECK environment variable to bypass the restriction globally:

# Shell invocation
OPENCLAW_NO_TTY_CHECK=1 openclaw models auth login --provider openai-codex

# Embedded in companion app (KatClaw example)
import { execSync } from 'child_process';

execSync('openclaw models auth login --provider openai-codex', {
  env: { ...process.env, OPENCLAW_NO_TTY_CHECK: '1' },
  stdio: 'inherit'
});

Patch to src/cli/auth-commands.ts:

// Add at top of file
const isTtyOverride = process.env.OPENCLAW_NO_TTY_CHECK === '1';

function requireInteractive(): void {
  if (!isTtyOverride && !process.stdin.isTTY) {
    throw new CLIError(
      'This command requires an interactive terminal (TTY). ' +
      'Run this command directly in your terminal to continue.'
    );
  }
}

Solution C: Companion App OAuth Token Injection

For applications that manage OAuth externally, directly write the token to the credentials store:

# Step 1: Extract OAuth token from your app's flow
# (This assumes you implement the PKCE flow independently)

# Step 2: Write token to OpenClaw credentials directory
cat > ~/.openclaw/credentials/openai-codex.json << 'EOF'
{
  "provider": "openai-codex",
  "access_token": "sk-...",
  "refresh_token": "rt-...",
  "expires_at": 1735689600000,
  "scope": "codex.connect"
}
EOF
chmod 600 ~/.openclaw/credentials/openai-codex.json

# Step 3: Verify credentials are recognized
openclaw models list --provider openai-codex

๐Ÿงช Verification

Verify Non-Interactive Mode Works

After applying the fix, test from a non-interactive context:

# Create a pseudo-TTY test environment
script -q /dev/null -c "openclaw models auth login --provider openai-codex --no-interactive" || true

# Expected behavior: Browser opens, process waits for callback
# Verify exit code handling
openclaw models auth login --provider openai-codex --no-interactive
echo "Exit code: $?"  # Should be 0 after successful callback or 124 if timeout

Verify Credential Storage

After completing the OAuth flow:

# Check credential file exists and has valid structure
$ cat ~/.openclaw/credentials/openai-codex.json | jq keys
[
  "provider",
  "access_token",
  "refresh_token",
  "expires_at",
  "scope"
]

# Verify token is not empty
$ cat ~/.openclaw/credentials/openai-codex.json | jq '.access_token | length'
52

# Test API access with stored credentials
$ openclaw models list --provider openai-codex
NAME         TYPE      CONTEXT WINDOW
gpt-4        chat      128000
gpt-4-turbo  chat      128000
gpt-4o       chat      128000
codex-latest code      200000

Verify Environment Variable Bypass

# Test with environment variable (no code changes required)
$ unset OPENCLAW_NO_TTY_CHECK
$ openclaw models auth login --provider openai-codex
Error: This command requires an interactive terminal (TTY).
$ export OPENCLAW_NO_TTY_CHECK=1
$ openclaw models auth login --provider openai-codex
[Browser opens for OAuth]
# Success: bypass works

Verify Companion App Token Injection

# After writing token manually
$ openclaw models auth status --provider openai-codex
Provider: openai-codex
Status: authenticated
Expires: 2025-01-01T00:00:00.000Z
Scopes: codex.connect

# Test actual API call
$ openclaw models invoke --provider openai-codex --model gpt-4o --prompt "test"
{
  "content": "test",
  "model": "gpt-4o",
  "usage": { "prompt_tokens": 3, "completion_tokens": 2 }
}

โš ๏ธ Common Pitfalls

1. Callback Port Conflicts

When running multiple instances, the default callback port 8765 may be occupied:

# Error: listen EADDRINUSE :::8765
# Solution: Specify alternate port
openclaw models auth login --provider openai-codex --callback-port 9876

2. Browser Opens in Wrong Environment

In SSH or remote sessions, the browser may open on the remote host instead of the local machine:

# For macOS remote sessions, use:
openclaw models auth login --provider openai-codex --browser macos-open

# For WSL/Windows cross-environment:
# Ensure DISPLAY is set correctly or use --browser wsl-launch

3. Stale Credential File Permissions

If credentials were previously written as root or with overly permissive permissions:

# Fix permissions
chmod 600 ~/.openclaw/credentials/openai-codex.json
chown $USER ~/.openclaw/credentials/openai-codex.json

# Verify
ls -la ~/.openclaw/credentials/openai-codex.json
# Expected: -rw------- (600 permissions)

4. Token Expiration During Long Operations

The refresh token may expire if the companion app doesn’t implement automatic renewal:

# Check expiration before long-running tasks
$ cat ~/.openclaw/credentials/openai-codex.json | jq '.expires_at'
1735689600000  # Unix timestamp in milliseconds

# Refresh if within 24 hours of expiration
openclaw models auth refresh --provider openai-codex

5. Docker Container Isolation

OAuth browser redirect cannot work from within a Docker container without proper configuration:

# Incorrect (container has no browser access)
docker run my-app openclaw models auth login --provider openai-codex

# Correct (use host network mode and expose callback port)
docker run --network host -p 8765:8765 \
  -e OPENCLAW_NO_TTY_CHECK=1 \
  my-app openclaw models auth login --provider openai-codex

# Alternative: Inject pre-obtained token via volume mount
docker run -v $HOME/.openclaw:/root/.openclaw:ro my-app

6. KatClaw Companion App Specific

When integrating with KatClaw, ensure the OpenClaw subprocess inherits the correct environment:

# Incorrect (drops GUI environment variables)
const child = spawn('openclaw', ['models', 'auth', 'login', '--provider', 'openai-codex'], {
  cwd: app.getPath('home')
});

# Correct (preserves environment for browser launch)
const child = spawn('openclaw', ['models', 'auth', 'login', '--provider', 'openai-codex'], {
  cwd: app.getPath('home'),
  env: { ...process.env, OPENCLAW_NO_TTY_CHECK: '1' },
  stdio: 'inherit'
});
  • EACCES credentials/unauthorized
    Symptom: Token present but API calls fail with 401.
    Cause: Revoked OAuth token or expired credentials file.
    Reference: src/auth/token-validator.ts
  • ECONNREFUSED callback-server
    Symptom: OAuth completes in browser but CLI reports callback failure.
    Cause: Firewall blocking localhost or port not listening.
    Reference: src/auth/callback-server.ts
  • ENOENT credentials file not found
    Symptom: openclaw models auth status reports no credentials.
    Cause: Credentials directory missing or not initialized.
    Reference: src/config/credentials-store.ts
  • ENOTTY stdin is not a terminal
    Symptom: Command fails with TTY-related error in CI environments.
    Cause: The primary issue addressed by this guide.
    Reference: src/cli/auth-commands.ts:requireInteractive()
  • INVALID_PROVIDER openai-codex
    Symptom: Unknown provider error despite valid subscription.
    Cause: Provider not registered in ~/.openclaw/providers.json.
    Reference: src/providers/registry.ts
  • GitHub Issue #447: "Onboarding wizard too verbose for quick auth"
    Symptom: Users bypassed full wizard but couldn't reach OAuth directly.
    Resolution: Added --auth-choice flag (partial solution).
    Reference: Tracked in docs/roadmap.md
  • GitHub Issue #892: "OAuth callback fails in WSL2"
    Symptom: Browser opens in Windows but callback never received by WSL CLI.
    Resolution: Added --browser wsl-launch option.
    Reference: src/auth/browser-detect.ts

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.