# How Whisper API Keys Are Loaded From Environment Variables vs Config Files in claude-video

> Discover how claude-video loads Whisper API keys from environment variables or config files. Learn the secure fallback method for API key management.

- Repository: [bradautomates/claude-video](https://github.com/bradautomates/claude-video)
- Tags: how-to-guide
- Published: 2026-08-10

---

**The `claude-video` repository loads Whisper API keys by first checking environment variables (`GROQ_API_KEY` or `OPENAI_API_KEY`), then falling back to a user-specific `~/.config/watch/.env` config file if the environment variables are unset.**

The `watch` skill implements a two-stage loading mechanism that prioritizes **environment variables** for CI/CD and containerized deployments while supporting a **local config file** for interactive development. This design keeps secrets out of version control without sacrificing developer convenience.

## Backend Selection Drives Key Loading

The loading process begins with backend selection. In [`skills/watch/scripts/watch.py`](https://github.com/bradautomates/claude-video/blob/main/skills/watch/scripts/watch.py) (lines 239-257), the `load_api_key()` function determines which provider to use based on the `--whisper` CLI flag:

- **Groq** — cheaper inference, preferred default
- **OpenAI** — fallback when Groq is unavailable

If no flag is provided, the code attempts Groq first and automatically falls back to OpenAI when a Groq key cannot be found.

## Two-Tier Key Resolution: Environment Variables First

The `load_api_key()` function in [`skills/watch/scripts/whisper.py`](https://github.com/bradautomates/claude-video/blob/main/skills/watch/scripts/whisper.py) implements ordered precedence:

| Backend | Environment Variable | Config File Key |
|---------|---------------------|---------------|
| Groq | `GROQ_API_KEY` | `groq_api_key` |
| OpenAI | `OPENAI_API_KEY` | `openai_api_key` |

### Step 1: Environment Variable Check

```python

# From skills/watch/scripts/whisper.py

import os

def load_api_key(preferred_backend=None):
    # Determine which backend to use

    backend = preferred_backend or "groq"
    
    # Map backend to environment variable name

    env_var_map = {
        "groq": "GROQ_API_KEY",
        "openai": "OPENAI_API_KEY"
    }
    
    # First: check environment variable

    api_key = os.getenv(env_var_map[backend])
    if api_key:
        return backend, api_key
    
    # Fall through to config file...

```

### Step 2: Config File Fallback

When the environment variable is absent, the function loads `~/.config/watch/.env` using **python-dotenv**:

```python

# From skills/watch/scripts/whisper.py (continued)

from pathlib import Path
from dotenv import dotenv_values

def _load_user_config():
    """Load user-specific configuration from ~/.config/watch/.env"""
    config_dir = Path.home() / ".config" / "watch"
    config_dir.mkdir(parents=True, exist_ok=True)
    
    env_path = config_dir / ".env"
    return dotenv_values(env_path) if env_path.exists() else {}

def load_api_key(preferred_backend=None):
    # ... environment variable check above ...

    
    # Second: check config file

    config = _load_user_config()
    config_key_map = {
        "groq": "groq_api_key",
        "openai": "openai_api_key"
    }
    
    api_key = config.get(config_key_map[backend])
    if api_key:
        return backend, api_key
    
    # No key found — return None to trigger error handling

    return None, None

```

The config directory path is centralized in [`skills/watch/scripts/config.py`](https://github.com/bradautomates/claude-video/blob/main/skills/watch/scripts/config.py) to ensure consistency across the codebase.

## Error Handling When Both Sources Fail

The caller in [`skills/watch/scripts/watch.py`](https://github.com/bradautomates/claude-video/blob/main/skills/watch/scripts/watch.py) handles missing keys gracefully:

```python

# From skills/watch/scripts/watch.py

from whisper import load_api_key, transcribe_video

backend, api_key = load_api_key(args.whisper)

if not api_key:
    print(
        f"[watch] whisper fallback failed: missing API key for {backend}",
        file=sys.stderr,
    )
    sys.exit(1)

# Proceed with transcription

transcript = transcribe_video(video_path, backend, api_key)

```

## Practical Configuration Examples

### Environment Variable Setup (Production/CI)

```bash

# Export for current session

export GROQ_API_KEY="gsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# Or use a .env file loaded by your orchestrator

docker run -e GROQ_API_KEY="$GROQ_API_KEY" claude-video watch "https://youtu.be/..."

```

### Config File Setup (Local Development)

```bash

# Create directory with restricted permissions

mkdir -p ~/.config/watch
chmod 700 ~/.config/watch

```

```bash

# ~/.config/watch/.env — keep this file private (chmod 600)

groq_api_key=gsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
openai_api_key=sk-openai-yyyyyyyyyyyyyyyyyyyyyyyyyyyy

```

```bash
chmod 600 ~/.config/watch/.env

```

### Using the CLI With Explicit Backend

```bash

# Use default (Groq with fallback to OpenAI)

watch "https://youtu.be/dQw4w9WgXcQ"

# Force OpenAI Whisper

watch "https://youtu.be/dQw4w9WgXcQ" --whisper openai

# Force Groq (no fallback)

watch "https://youtu.be/dQw4w9WgXcQ" --whisper groq

```

## Key Source Files

| File | Purpose |
|------|---------|
| [`skills/watch/scripts/whisper.py`](https://github.com/bradautomates/claude-video/blob/main/skills/watch/scripts/whisper.py) | Implements `load_api_key()` and backend-specific transcription logic |
| [`skills/watch/scripts/watch.py`](https://github.com/bradautomates/claude-video/blob/main/skills/watch/scripts/watch.py) | CLI entry point that calls `load_api_key()` and handles errors |
| [`skills/watch/scripts/config.py`](https://github.com/bradautomates/claude-video/blob/main/skills/watch/scripts/config.py) | Centralizes paths including `~/.config/watch` directory |
| [`tests/test_whisper.py`](https://github.com/bradautomates/claude-video/blob/main/tests/test_whisper.py) | Validates key loading, chunking, and error scenarios |

## Summary

- **Environment variables take precedence** — set `GROQ_API_KEY` or `OPENAI_API_KEY` for deployment scenarios
- **Config file provides convenience** — store keys in `~/.config/watch/.env` for local development
- **Backend selection is flexible** — use `--whisper` flag or accept automatic Groq→OpenAI fallback
- **Graceful degradation** — missing keys produce clear error messages rather than cryptic failures
- **Security-conscious defaults** — config directory creation with proper permissions, no keys in repository

## Frequently Asked Questions

### What happens if both GROQ_API_KEY and OPENAI_API_KEY are set?

The code uses **backend selection logic first**, not key availability. If you don't specify `--whisper`, it attempts Groq (`GROQ_API_KEY`). If you pass `--whisper openai`, it uses `OPENAI_API_KEY` regardless of whether Groq credentials exist.

### Can I use a custom config file location?

Not directly through the current implementation. The path `~/.config/watch/.env` is hardcoded in [`skills/watch/scripts/config.py`](https://github.com/bradautomates/claude-video/blob/main/skills/watch/scripts/config.py). To override, set the appropriate environment variable instead, which always takes precedence.

### Why does the code prefer Groq over OpenAI?

According to source comments in [`watch.py`](https://github.com/bradautomates/claude-video/blob/main/watch.py), Groq offers **significantly cheaper inference** for Whisper transcription. The automatic fallback to OpenAI ensures functionality when Groq rate limits or availability issues occur.