Menu Close

Leveraging multiple, repository-specific OpenAI Codex API Keys with Visual Studio Code on macOS

When working across multiple repositories, a single, global API key quickly becomes painful. This practical workflow makes per-repo keys feel native.

Background

OpenAI Codex

OpenAI’s Codex has evolved well beyond its autocomplete origins into a fully autonomous coding agent — one that interacts with real codebases, executes commands, and manages development tasks across tools and environments. Think less pair-programmer and more delegated implementer.

Visual Studio Code Integration

On macOS, Codex integrates directly into Visual Studio Code via an extension that embeds the agent in the editor sidebar — enabling natural-language-driven code generation, editing, and debugging within your active workspace. You can also connect the ChatGPT macOS app to VS Code for deeper, file-aware interaction without leaving your editor.

Challenge

A current vendor limitation introduces friction for multi-repo workflows, as developers must manually overwrite the single, plain-text key, rather than natively scoping pre-project credentials.

Leveraging multiple, repository-specific OpenAI Codex API keys in Visual Studio Code on macOS is constrained by Codex’s reliance on a single, global credential file at ~/.codex/auth.json, where authentication state and your API Key — displayed in plain-text — are centrally stored.

grep OPENAI_API_KEY ~/.codex/auth.json

Approach

1. Installation

This approach relies on access to and the installation of the following prerequisites (preferably installed via your organization’s custom app store):

1.1 OpenAI Platform

Once you’ve been granted access to your organization’s OpenAI Platform account, create multiple, repository-specific API Keys.

Create one dedicated API key per repository or project (e.g., repo-frontend-prod, repo-backend-staging).

Give each key a descriptive name and restrict its usage to specific projects or rate limits where possible. This scoping helps with cost tracking, security auditing, and preventing one compromised key from affecting all your work.

Copy each key immediately after creation — you won’t be able to view it again in the dashboard. Store them securely in 1Password (detailed in the next section) rather than in any local files or notes.

1.2 1Password

To avoid exposing API Keys in plain text, this approach relies on the 1Password desktop app and the 1Password command-line interface

Securely store each API Key secret in the 1Password desktop app, following a predictable naming convention, using the recommended option of API Credentials.

Use the recommended option of "API Credentials"

While still in the 1Password desktop app, enable: Settings > Developer > Command-Line Interface (CLI) > Integrate with 1Password CLI

In the 1Password desktop app, enable: Settings > Developer > Command-Line Interface (CLI) > Integrate with 1Password CLI
1.3 OpenAI Codex

Access to OpenAI Codex is a hard-requirement for this approach.

Ensure you have an active subscription or API credits that grant access to Codex.

The Codex CLI (codex binary) and the official Codex IDE Extension for Visual Studio Code both support authentication via OpenAI API keys.

This approach works best when you authenticate using API keys (rather than ChatGPT account login) because it gives you full control over which key is active in each repository’s environment.

1.4 Visual Studio Code

While this approach will most likely work with other IDEs, Visual Studio Code is the author’s preferred option and the Codex IDE Extension offers significant quality-of-life features.

Install the official Codex – OpenAI’s coding agent extension from the VS Code Marketplace. It embeds a powerful sidebar panel for chatting with Codex, context-aware code editing, task delegation, and more — all directly inside your workspace.

1.5 Homebrew

The Missing Package Manager for macOS, Homebrew, should be installed via your organization’s custom app store (if not already present).

Install the three required tools with these commands:

brew install --cask codex
brew install --cask 1password-cli
brew install direnv

Or as a convenient one-liner:

brew install --cask codex 1password-cli && brew install direnv

Verify everything installed correctly:

codex --version
op --version
direnv --version

Note: codex and 1password-cli install as casks, while direnv installs as a regular formula. If you previously installed Codex via the old formula method, you may need to run brew uninstall codex --formula first before installing the cask.

2. Configuration

With the prerequisite installations in-place, complete the following, one-time configuration.

2.1 1password-cli

In Terminal, leverage the op binary to output a list of your vaults, authenticating when prompted:

op vault list
In Terminal, leverage the op binary to output a list of your vaults, authenticating when prompted:
2.2 direnv

Integrate direnv into your shell so it can automatically load / unload environments when you cd into directories by adding the following line to the end of your ~/.zshrc file:

echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc

Close and reopen a Terminal window (or reload your shell via source ~/.zshrc):

direnv --version
2.3 codex

First, confirm codex is installed via the following:

codex --version

Next — for confirmation of the previously claimed challenge only — log in to the Codex CLI (OpenAI’s coding agent) using an API key, via the dedicated codex login command with the --with-api-key flag. This reads the key from stdin, which works well for security (especially in scripts or CI/CD) and avoids passing it directly in the command line.

export OPENAI_API_KEY="sk-your-api-key-here"
echo "$OPENAI_API_KEY" | codex login --with-api-key

Confirm the API key is in-use (use Control-D to exit):

codex "say hello and confirm API usage"

Confirm Codex’s reliance on a single, global, plain-text API key:

grep OPENAI_API_KEY ~/.codex/auth.json

Manually logout of Codex and confirm the API key is no longer present:

codex logout

grep OPENAI_API_KEY ~/.codex/auth.json
2.4 ~/.zshrc

See my earlier post bz please for one way to manage custom shell functions.

Add the following helper functions to your ~/.zshrc file, which should simplify daily use.

OpenAI Codex ~/.zshrc Functions

Be sure to update the following based on your API Keys:

OPENAI_GENERAL_ITEM_NAME="${OPENAI_GENERAL_ITEM_NAME:-Example OpenAI General Key}"

# Update `_key_names` to match your masked API key suffixes.
local -A _key_names=(
[kPqX]="Microsoft 365 Reset"
[vL7m]="Mac Health Check"
[rT9p]="DDM OS Reminder"
[xY2k]="Setup Your Mac"
[bF8v]="General"
)
# These functions will need to be manually added to your `~/.zshrc` file to work properly.
# See: [bz please](https://snelson.us/2022/05/bz-please/)

# --- OpenAI Codex: Start -----------------------------------------------------

OPENAI_GENERAL_ITEM_NAME="${OPENAI_GENERAL_ITEM_NAME:-Example OpenAI General Key}"

_oaki () {   # [o]pen[a]i [k]ey [i]dentity — resolve Name from last-4 of key
    local key="${1:-${OPENAI_API_KEY:-}}"
    [[ -n "$key" ]] || {
        printf "_oaki: no key provided and OPENAI_API_KEY is unset\n" >&2
        return 1
    }

    local -A _key_names=(
        [kPqX]="Microsoft 365 Reset"
        [vL7m]="Mac Health Check"
        [rT9p]="DDM OS Reminder"
        [xY2k]="Setup Your Mac"
        [bF8v]="General"
    )

    local suffix="${key[-4,-1]}"
    local name="${_key_names[$suffix]}"
    if [[ -z "$name" ]]; then
        printf "\033[1;31m_oaki: unrecognized key suffix:\033[0m %s\n" "$suffix" >&2
        return 1
    fi

    printf "%s" "$name"
}

_codex_usage () {
    printf "Usage: %s\n" "$1" >&2
}

_codex_format_key_display () {
    local key="${1:-}"
    local allow_unknown="${2:---allow-unknown}"
    local key_name

    [[ -n "$key" ]] || {
        printf "_codex_format_key_display: key is required\n" >&2
        return 1
    }

    if ! key_name="$(_oaki "$key" 2>/dev/null)"; then
        [[ "$allow_unknown" == "--allow-unknown" ]] || return 1
        key_name="(unknown)"
    fi

    printf "%s (%s…%s)" "$key_name" "${key[1,3]}" "${key[-4,-1]}"
}

_codex_usage_limit_file () {
    printf "%s" "${HOME}/.codex/cul-deadline"
}

_codex_parse_usage_limit_deadline () {
    local message="${1:-}"
    local marker="try again at "
    local deadline_text
    local normalized_deadline
    local deadline_epoch

    [[ -n "$message" ]] || {
        printf "cul: usage-limit message is required\n" >&2
        return 1
    }

    deadline_text="${message#*${marker}}"
    if [[ "$deadline_text" == "$message" ]]; then
        printf "cul: could not find 'try again at ' in the provided message\n" >&2
        return 1
    fi

    deadline_text="${deadline_text%.}"
    normalized_deadline="$(printf "%s\n" "$deadline_text" | sed -E 's/([[:digit:]]+)(st|nd|rd|th)/\1/g')"

    if ! deadline_epoch="$(LC_TIME=C date -j -f "%b %e, %Y %l:%M %p" -v0S "$normalized_deadline" "+%s" 2>/dev/null)"; then
        printf "cul: could not parse usage-limit deadline: %s\n" "$deadline_text" >&2
        return 1
    fi

    printf "%s" "$deadline_epoch"
}

_codex_store_usage_limit_deadline () {
    local deadline_epoch="${1:-}"
    local deadline_file="$(_codex_usage_limit_file)"

    [[ "$deadline_epoch" == <-> ]] || {
        printf "_codex_store_usage_limit_deadline: deadline epoch is required\n" >&2
        return 1
    }

    mkdir -p "${deadline_file:h}" || {
        printf "cul: could not create %s\n" "${deadline_file:h}" >&2
        return 1
    }

    printf "%s\n" "$deadline_epoch" >| "$deadline_file" || {
        printf "cul: could not write %s\n" "$deadline_file" >&2
        return 1
    }
}

_codex_load_usage_limit_deadline () {
    local deadline_file="$(_codex_usage_limit_file)"
    local deadline_epoch

    [[ -r "$deadline_file" ]] || return 1
    deadline_epoch="$(<"$deadline_file")"
    [[ "$deadline_epoch" == <-> ]] || return 1

    printf "%s" "$deadline_epoch"
}

_codex_clear_usage_limit_deadline () {
    local deadline_file="$(_codex_usage_limit_file)"

    [[ -e "$deadline_file" ]] || return 0
    rm -f "$deadline_file" || {
        printf "cul: could not remove %s\n" "$deadline_file" >&2
        return 1
    }
}

_codex_print_usage_limit_guidance () {
    local deadline_epoch="${1:-}"
    local deadline_display
    local now_epoch

    [[ "$deadline_epoch" == <-> ]] || {
        print -P "%F{red}_codex_print_usage_limit_guidance: deadline epoch is required%f" >&2
        return 1
    }

    if ! deadline_display="$(LC_TIME=C date -r "$deadline_epoch" "+%b %e, %Y %l:%M %p %Z" 2>/dev/null)"; then
        print -P "%F{red}_codex_print_usage_limit_guidance: could not format deadline%f" >&2
        return 1
    fi

    now_epoch="$(date "+%s")" || return 1

    if (( now_epoch < deadline_epoch )); then
        print -P "%F{red}Usage-limit deadline: ${deadline_display}%f. Keep using this API key until then, then run '%F{yellow}cul --clear%f'."
        return 0
    fi

    print -P "%F{red}Usage-limit deadline:%f %F{green}${deadline_display} has passed%f. Run '%F{yellow}clo%f' now."
}

_codex_login_with_key () {
    local key="${1:-}"
    local key_display

    [[ -n "$key" ]] || {
        printf "_codex_login_with_key: key is required\n" >&2
        return 1
    }

    setopt localoptions pipefail
    if ! print -rn -- "${key}" | codex login --with-api-key; then
        if key_display="$(_codex_format_key_display "$key")"; then
            printf "Failed to authenticate with codex using %s.\n" "$key_display" >&2
        else
            printf "Failed to authenticate with codex using the provided API key.\n" >&2
        fi
        return 1
    fi

    if ! key_display="$(_codex_format_key_display "$key" --strict)"; then
        printf "\ncli: could not resolve key name; proceeding with masked key only.\n" >&2
        key_display="(unknown) (${key[1,3]}…${key[-4,-1]})"
    fi

    printf "\n\033[2mUsing the following OpenAI API Key:\033[0m \033[1;34m%s\033[0m\n" "$key_display"

    if ! print -rn -- "${key}" | pbcopy; then
        printf "Failed to copy API key to clipboard.\n" >&2
        return 1
    fi

    printf "\n\033[1;31mCAUTION:\033[0m The entire API Key has been copied to the clipboard (so that you can easily login to the Codex Extension).\n"
    printf "\nRemember to 'clo' ([c]odex [l]og[o]ut) at the end of every Visual Studio Code session.\n\n"
}

_codex_read_general_key () {
    local general_key

    printf "Loading %s API Key from 1Password …\n" "${OPENAI_GENERAL_ITEM_NAME}" >&2
    if ! general_key="$(op read "op://Employee/${OPENAI_GENERAL_ITEM_NAME}/password" --no-newline 2>/dev/null)"; then
        printf "Failed to read the %s API Key from 1Password.\n" "${OPENAI_GENERAL_ITEM_NAME}" >&2
        return 1
    fi

    [[ -n "$general_key" ]] || {
        printf "The %s API Key read from 1Password was empty.\n" "${OPENAI_GENERAL_ITEM_NAME}" >&2
        return 1
    }

    printf "%s" "$general_key"
}

_codex_continue_env_file () {
    printf "%s" "${HOME}/.continue/.env"
}

_codex_sync_continue_env_key () {
    local key="${1:-${OPENAI_API_KEY:-}}"
    local env_file="$(_codex_continue_env_file)"
    local env_dir="${env_file:h}"
    local tmp_file="${env_file}.tmp.$$"
    local line
    local found=0

    [[ -n "$key" ]] || {
        printf "_codex_sync_continue_env_key: key is required\n" >&2
        return 1
    }

    mkdir -p "$env_dir" || {
        printf "Failed to create %s.\n" "$env_dir" >&2
        return 1
    }

    : >| "$tmp_file" || {
        printf "Failed to create temporary file for %s.\n" "$env_file" >&2
        return 1
    }

    if [[ -r "$env_file" ]]; then
        while IFS= read -r line || [[ -n "$line" ]]; do
            if [[ "$line" == OPENAI_API_KEY=* ]]; then
                printf "OPENAI_API_KEY=%s\n" "$key" >> "$tmp_file" || return 1
                found=1
            else
                printf "%s\n" "$line" >> "$tmp_file" || return 1
            fi
        done < "$env_file"
    fi

    if (( ! found )); then
        printf "OPENAI_API_KEY=%s\n" "$key" >> "$tmp_file" || return 1
    fi

    mv "$tmp_file" "$env_file" || {
        rm -f "$tmp_file"
        printf "Failed to update %s.\n" "$env_file" >&2
        return 1
    }

    chmod 600 "$env_file" 2>/dev/null || true
}

_codex_clear_continue_env_key () {
    local env_file="$(_codex_continue_env_file)"
    local tmp_file="${env_file}.tmp.$$"
    local line
    local kept=0

    [[ -e "$env_file" ]] || return 0

    : >| "$tmp_file" || {
        printf "Failed to create temporary file for %s.\n" "$env_file" >&2
        return 1
    }

    while IFS= read -r line || [[ -n "$line" ]]; do
        if [[ "$line" == OPENAI_API_KEY=* ]]; then
            continue
        fi
        printf "%s\n" "$line" >> "$tmp_file" || return 1
        kept=1
    done < "$env_file"

    if (( kept )); then
        mv "$tmp_file" "$env_file" || {
            rm -f "$tmp_file"
            printf "Failed to update %s.\n" "$env_file" >&2
            return 1
        }
    else
        rm -f "$tmp_file" "$env_file" || {
            printf "Failed to remove %s.\n" "$env_file" >&2
            return 1
        }
    fi
}

_codex_load_auth_state () {
    local auth_file="${HOME}/.codex/auth.json"
    local auth_json

    typeset -g _CODEX_AUTH_LOADED=0
    typeset -g _CODEX_AUTH_MODE=""
    typeset -g _CODEX_AUTH_KEY=""

    [[ -r "$auth_file" ]] || return 1
    auth_json="$(<"$auth_file")"
    [[ -n "$auth_json" ]] || return 1

    _CODEX_AUTH_LOADED=1
    _CODEX_AUTH_MODE="$(printf "%s\n" "$auth_json" | sed -n 's/^[[:space:]]*"auth_mode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
    _CODEX_AUTH_KEY="$(printf "%s\n" "$auth_json" | sed -n 's/^[[:space:]]*"OPENAI_API_KEY"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
}

_codex_print_shell_env_status () {
    local key="${OPENAI_API_KEY:-}"

    if [[ -z "$key" ]]; then
        printf "\033[2mRepo key loaded in shell:\033[0m none (OPENAI_API_KEY is unset)\n"
        return 0
    fi

    printf "\033[2mRepo key loaded in shell:\033[0m \033[1;34m%s\033[0m\n" "$(_codex_format_key_display "$key")"
}

_codex_print_auth_file_status () {
    if [[ -n "$_CODEX_AUTH_KEY" ]]; then
        if [[ -n "$_CODEX_AUTH_MODE" ]]; then
            printf "\033[2mCodex auth file:\033[0m \033[1;34m%s -> %s\033[0m\n" "$_CODEX_AUTH_MODE" "$(_codex_format_key_display "$_CODEX_AUTH_KEY")"
        else
            printf "\033[2mCodex auth file:\033[0m \033[1;34m%s\033[0m\n" "$(_codex_format_key_display "$_CODEX_AUTH_KEY")"
        fi
        return 0
    fi

    if [[ -n "$_CODEX_AUTH_MODE" ]]; then
        printf "\033[2mCodex auth file:\033[0m \033[1;34m%s (no stored API key)\033[0m\n" "$_CODEX_AUTH_MODE"
        return 0
    fi

    printf "\033[2mCodex auth file:\033[0m \033[1;34msigned out\033[0m\n"
}

_codex_print_state_mismatch_note () {
    local shell_key="${OPENAI_API_KEY:-}"

    [[ "$_CODEX_AUTH_LOADED" -eq 1 ]] || return 0

    if [[ -n "$shell_key" && -z "$_CODEX_AUTH_KEY" ]]; then
        if [[ "$_CODEX_AUTH_MODE" == "chatgpt" ]]; then
            printf "\033[1;31mMismatch:\033[0m a repo key is loaded in the shell, but Codex auth file is chatgpt with no stored API key.\n"
        elif [[ -n "$_CODEX_AUTH_MODE" ]]; then
            printf "\033[1;31mMismatch:\033[0m a repo key is loaded in the shell, but Codex auth file is %s with no stored API key.\n" "$_CODEX_AUTH_MODE"
        else
            printf "\033[1;31mMismatch:\033[0m a repo key is loaded in the shell, but Codex auth file is signed out with no stored API key.\n"
        fi
        return 0
    fi

    if [[ -z "$shell_key" && -n "$_CODEX_AUTH_KEY" ]]; then
        if [[ -n "$_CODEX_AUTH_MODE" ]]; then
            printf "\033[1;31mMismatch:\033[0m no repo key is loaded in the shell, but Codex auth file has a stored API key under %s.\n" "$_CODEX_AUTH_MODE"
        else
            printf "\033[1;31mMismatch:\033[0m no repo key is loaded in the shell, but Codex auth file has a stored API key.\n"
        fi
        return 0
    fi

    if [[ -n "$shell_key" && -n "$_CODEX_AUTH_KEY" && "$shell_key" != "$_CODEX_AUTH_KEY" ]]; then
        printf "\033[1;31mMismatch:\033[0m the repo key loaded in the shell differs from the API key stored in Codex auth.\n"
    fi
}

_codex_print_source_of_truth_note () {
    printf "\033[1;33mNote:\033[0m the shell can load OPENAI_API_KEY independently; Codex persists login state in \033[1;34mjq .OPENAI_API_KEY ~/.codex/auth.json\033[0m.\n"
}

_codex_resolve_login_key () {
    local mode="${1:-}"
    local key

    case "$mode" in
        "")
            if [[ -n "${OPENAI_API_KEY:-}" ]]; then
                printf "%s" "${OPENAI_API_KEY}"
                return 0
            fi

            printf "OPENAI_API_KEY is not set; attempting to load from direnv…\n" >&2
            key="$(direnv exec . sh -c 'printf "%s" "$OPENAI_API_KEY"' 2>/dev/null)"
            if [[ -z "$key" ]]; then
                printf "OPENAI_API_KEY is still not set — is there an .envrc in the current directory?\n" >&2
                return 1
            fi

            printf "%s" "$key"
            ;;
        "--general")
            _codex_read_general_key
            ;;
        *)
            _codex_usage "cli [--general]"
            return 1
            ;;
    esac
}

_codex_logout () {
    if ! codex logout; then
        printf "codex logout failed.\n" >&2
        return 1
    fi
}

cli () {     # [c]odex [l]og[i]n
    local mode="${1:-}"
    local key
    local deadline_epoch

    if ! key="$(_codex_resolve_login_key "$mode")"; then
        return 1
    fi

    if [[ "$mode" != "--general" && -z "${OPENAI_API_KEY:-}" ]]; then
        OPENAI_API_KEY="$key"
        export OPENAI_API_KEY
    fi

    _codex_login_with_key "$key" || return 1
    _codex_sync_continue_env_key "$key" || return 1

    if deadline_epoch="$(_codex_load_usage_limit_deadline)"; then
        _codex_print_usage_limit_guidance "$deadline_epoch"
    fi
}

clo () {     # [c]odex [l]og[o]ut
    if (( $# > 0 )); then
        _codex_usage "clo"
        return 1
    fi

    if [[ -n "${OPENAI_API_KEY:-}" ]]; then
        printf "Clearing OpenAI API Key …\n" >&2
        unset OPENAI_API_KEY || {
            printf "Failed to unset OPENAI_API_KEY.\n" >&2
            return 1
        }
    fi

    _codex_clear_continue_env_key || return 1

    _codex_logout || return 1
    printf "\nRemember to 'Cmd + Shift + P → Developer: Restart Extension Host [ ⌘ ⇧ ⌥ R ]' before logging in again.\n\n"
}

cul () {     # [c]odex [u]sage-[l]imit
    local usage="cul [--clear | '<usage-limit-message>']"
    local deadline_epoch

    (( $# == 1 )) || {
        _codex_usage "$usage"
        return 1
    }

    if [[ "$1" == "--clear" ]]; then
        _codex_clear_usage_limit_deadline || return 1
        printf "Cleared stored usage-limit reminder.\n"
        return 0
    fi

    if ! deadline_epoch="$(_codex_parse_usage_limit_deadline "$1")"; then
        return 1
    fi

    _codex_store_usage_limit_deadline "$deadline_epoch" || return 1
    _codex_print_usage_limit_guidance "$deadline_epoch"
}

cra () {     # [c]odex [r]e-[a]uthenticate
    _codex_logout || return 1
    cli
}

cai () {     # [c]odex [a]pi [i]nfo
    local usage="cai [--shell | --codex | <api-key>]"
    local key

    case "${1:-}" in
        "")
            (( $# == 0 )) || {
                _codex_usage "$usage"
                return 1
            }

            _codex_load_auth_state 2>/dev/null || true
            _codex_print_shell_env_status
            _codex_print_auth_file_status
            _codex_print_state_mismatch_note
            _codex_print_source_of_truth_note
            return 0
            ;;
        "--shell")
            (( $# == 1 )) || {
                _codex_usage "$usage"
                return 1
            }

            _codex_print_shell_env_status
            return 0
            ;;
        "--codex")
            (( $# == 1 )) || {
                _codex_usage "$usage"
                return 1
            }

            _codex_load_auth_state 2>/dev/null || true
            _codex_print_auth_file_status
            return 0
            ;;
        --*)
            _codex_usage "$usage"
            return 1
            ;;
    esac

    (( $# == 1 )) || {
        _codex_usage "$usage"
        return 1
    }

    key="$1"
    [[ -n "$key" ]] || {
        printf "cai: key is required\n" >&2
        return 1
    }

    if ! key="$(_codex_format_key_display "$key")"; then
        printf "cai: could not format the provided OpenAI API key\n" >&2
        return 1
    fi

    printf "OpenAI API Key: %s\n" "$key"
}

# --- OpenAI Codex: End -------------------------------------------------------

After editing and saving ~/.zshrc, reload your shell with source ~/.zshrc (or close and reopen Terminal).

CommandDescription
cliLog in using the currently-active OPENAI_API_KEY (or load it via direnv from .envrc if unset). Copies the full key to clipboard for the VS Code Codex extension.
cli --generalLoad and use the general-purpose OpenAI API key stored in 1Password (item name defined in $OPENAI_GENERAL_ITEM_NAME). Useful when no repo-specific key is needed.
cloLog out of Codex and clear the OPENAI_API_KEY from the current shell.
culParse a pasted Codex usage-limit message, treat its timestamp as local machine time, and store the last parsed deadline for later cli reminders. Before the deadline it reminds you to keep using the key until then and later clear the reminder with cul --clear; after the deadline it tells you to run clo.
craRe-authenticate (logout + login).
caiShow shell OPENAI_API_KEY status and Codex auth status. Supports --shell, --codex, and cai for explicit key lookup.

2.5 Repository

In each repository root, create the following:

.envrc (loads the repo-specific key from 1Password):

# .envrc – NEVER commit this file
# Get the exact path by right-clicking the secret in 1Password → Copy Reference
export OPENAI_API_KEY=$(op read "op://Private/Codex-Frontend-Prod/api-key" --no-newline)

.codex/config.toml (project-local Codex settings):

mkdir -p .codex && cat > .codex/config.toml << 'EOF'
# Project-local Codex settings – more secure and reliable with per-repo keys
cli_auth_credentials_store = "keyring" # Store credentials in macOS Keychain
preferred_auth_method = "apikey" # Help VS Code extension use the env var
EOF

Add the following to your .gitignore (create the file if it doesn’t exist):

.envrc 
.codex/

Then allow direnv:

direnv allow
3. Workflow

The following workflow should ease your day-to-day usage of repository-specific API Keys

One Workflow to Rule them All. Per-repository API Key flow: From vault to VS Code.
One-Workflow-to-Rule-them-All.pdf
3.1 Authenticating with Codex

In VS Code, select: File > Open Workspace from File…, then select View > Terminal.

direnv automatically loads the correct OPENAI_API_KEY from 1Password, prompting an authentication request:

direnv automatically loads the correct OPENAI_API_KEY from 1Password, prompting an authentication request

Login to Codex:

cli

In the Codex side panel, click Use API Key, paste the entire API Key and click OK. (If necessary, restart the extension host with Cmd + Shift + P → Developer: Restart Extension Host).

At any time, you can validate which API Key is currently in-use:

cai

When you’re finished with your current project, remember to logout of Codex:

clo
3.2 Daily Usage Tips

This combination (1Password + direnv + project-local config + shell functions) turns the global credential limitation into a smooth, secure, per-repository experience.

  • Always cd into the repository so direnv + your functions work automatically.
  • Use cli to authenticate (it copies the key for the VS Code extension).
  • At the end of every VS Code session, run clo (clears the key and logs out).
  • To re-authenticate quickly: cra
  • Check which key is active: cai
  • Use clear 1Password item names like Codex - frontend-prod
  • Periodically review active keys in the OpenAI dashboard and rotate them as needed.
3.3 Troubleshooting
  • direnv not loading the key? → Run direnv allow again
  • Extension stuck on old key? → Run clo, restart Extension Host, then cli
  • Unrecognized key suffix in functions? → Update the _key_names array
  • Keyring not working? → Delete ~/.codex/auth.json and re-authenticate

Feedback

This setup has dramatically reduced friction in my multi-repo workflows while keeping API keys secure and scoped.

Community-based, best-effort support is available on the Mac Admins Slack (free, registration required). You’re welcome to @ mention me — @dan-snelson — in a public channel if your mileage varies too greatly.

Bonus

apfel – Your Mac Already Has AI

Your Mac already has AI. Apple ships a language model with macOS -apfel unlocks it with one brew install. No downloads, no API keys, no config. The fastest path to local AI.

Local Setup with Visual Studio Code

Posted in AI, Tips & Tricks

Related Posts