
Doppler Secret Validation
- 124 installs
- 62 repo stars
- Updated August 3, 2026
- terrylica/cc-skills
Use doppler-secret-validation for development tasks
About
doppler-secret-validation: A skill for development. This provides functionality for development workflows.
- doppler-secret-validation
Doppler Secret Validation by the numbers
- 124 all-time installs (skills.sh)
- Ranked #2,799 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/terrylica/cc-skills --skill doppler-secret-validationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 124 |
|---|---|
| repo stars | ★ 62 |
| Last updated | August 3, 2026 |
| Repository | terrylica/cc-skills ↗ |
What it does
Use doppler-secret-validation for development tasks
Files
Doppler Secret Validation
Self-Evolving Skill: This skill improves through use. If instructions are wrong, parameters drifted, or a workaround was needed — fix this file immediately, don't defer. Only update for real, reproducible issues.
Overview
Workflow for securely adding, validating, and testing API tokens and credentials in Doppler secrets management.
When to Use This Skill
Use this skill when:
- User provides API tokens or credentials (PyPI, GitHub, AWS, etc.)
- User mentions "add to Doppler", "store secret", "validate token"
- User wants to test authentication before production use
- User needs to verify secret storage and retrieval
Workflow
Step 1: Test Token Format (Before Adding to Doppler)
Before storing in Doppler, validate token format:
# Check token format, length, prefix
python3 -c "token = 'TOKEN_VALUE'; print(f'Prefix: {token[:20]}...'); print(f'Length: {len(token)}')"Common token formats:
- PyPI:
pypi-...(179 chars) - GitHub:
ghp_...(40+ chars) - AWS: 20-char access key + 40-char secret
Step 2: Add Secret to Doppler
doppler secrets set SECRET_NAME="value" --project PROJECT --config CONFIGExample:
doppler secrets set PYPI_TOKEN="pypi-AgEI..." \
--project claude-config --config prdImportant: CLI doesn't support --note. Add notes via dashboard:
1. <https://dashboard.doppler.com> 2. Navigate: PROJECT → CONFIG → SECRET_NAME 3. Edit → Add descriptive note
Step 3: Validate Storage
Use the bundled validation script:
/usr/bin/env bash << 'VALIDATE_EOF'
cd ${CLAUDE_PLUGIN_ROOT}/skills/doppler-secret-validation
uv run scripts/validate_secret.py \
--project PROJECT \
--config CONFIG \
--secret SECRET_NAME
VALIDATE_EOFThis validates:
1. Secret exists in Doppler 2. Secret retrieval works 3. Environment injection works via doppler run
Example:
uv run scripts/validate_secret.py \
--project claude-config \
--config prd \
--secret PYPI_TOKENStep 4: Test API Authentication
Use the bundled auth test script (adapt test_api_authentication() for specific API):
/usr/bin/env bash << 'CONFIG_EOF'
cd ${CLAUDE_PLUGIN_ROOT}/skills/doppler-secret-validation
doppler run --project PROJECT --config CONFIG -- \
uv run scripts/test_api_auth.py \
--secret SECRET_NAME \
--api-url API_ENDPOINT
CONFIG_EOFExample (PyPI):
doppler run --project claude-config --config prd -- \
uv run scripts/test_api_auth.py \
--secret PYPI_TOKEN \
--api-url https://upload.pypi.org/legacy/Step 5: Document Usage
After validation, document the usage pattern for the user:
/usr/bin/env bash << 'CONFIG_EOF_2'
# Pattern 1: Doppler run (recommended for CI/scripts)
doppler run --project PROJECT --config CONFIG -- COMMAND
# Pattern 2: Manual export (for troubleshooting)
export SECRET_NAME=$(doppler secrets get SECRET_NAME \
--project PROJECT --config CONFIG --plain)
CONFIG_EOF_2Step 5b: mise [env] Integration (Recommended for Local Development)
For multi-account GitHub setups or per-directory credential needs, integrate Doppler secrets with mise [env]:
# .mise.toml
[env]
# Option A: Direct Doppler CLI fetch (slower, always fresh)
GH_TOKEN = "{{ exec(command='doppler secrets get GH_TOKEN --project myproject --config prd --plain') }}"
GITHUB_TOKEN = "{{ exec(command='doppler secrets get GH_TOKEN --project myproject --config prd --plain') }}"
# Option B: Cache for performance (1 hour cache)
GH_TOKEN = "{{ cache(key='gh_token', duration='1h', run='doppler secrets get GH_TOKEN --project myproject --config prd --plain') }}"
GITHUB_TOKEN = "{{ cache(key='gh_token', duration='1h', run='doppler secrets get GH_TOKEN --project myproject --config prd --plain') }}"Note: Set BOTH GH_TOKEN and GITHUB_TOKEN - different tools check different variable names (gh CLI vs npm scripts).
Why mise [env]? Doppler doppler run is session-scoped; mise [env] provides directory-scoped credentials that persist across commands.
See `mise-configuration` skill for complete patterns.
Common Patterns
Multiple Configs (dev, stg, prd)
Add secret to multiple environments:
# Production
doppler secrets set TOKEN="prod-value" --project foo --config prd
# Development
doppler secrets set TOKEN="dev-value" --project foo --config devVerify Secret Across Configs
/usr/bin/env bash << 'CONFIG_EOF_3'
for config in dev stg prd; do
echo "=== $config ==="
doppler secrets get TOKEN --project foo --config $config --plain | head -c 20
echo "..."
done
CONFIG_EOF_3Security Guidelines
1. Never log full secrets: Use ${SECRET:0:20}... masking 2. Prefer doppler run: Scopes secrets to single command 3. Use --plain only for piping: Human-readable view masks secrets 4. Separate configs per environment: dev/stg/prd isolation
Bundled Resources
- scripts/validate_secret.py - Complete validation suite (existence, retrieval, injection)
- scripts/test_api_auth.py - Template for API authentication testing
- references/doppler-patterns.md - Common CLI patterns and examples
Reference
- Doppler docs: <https://docs.doppler.com/docs>
- CLI install:
brew install dopplerhq/cli/doppler - See doppler-patterns.md for comprehensive patterns
---
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Secret not found | Wrong project/config specified | Verify with doppler secrets ls --project X --config |
| Auth test fails with 401 | Token expired or invalid | Regenerate token, re-add to Doppler |
| doppler run hangs | CLI waiting for input | Add --no-interactive flag |
| Token prefix mismatch | Wrong token type used | Check expected format (pypi-, ghp-, AKIA, etc.) |
| Validation script not found | Wrong directory context | Ensure CLAUDE_PLUGIN_ROOT is set correctly |
| Secret retrieval empty | Secret name typo | List secrets: doppler secrets ls --project X |
| mise cache stale | Duration expired | Clear cache or reduce duration setting |
| Multiple configs confusion | Secrets differ across envs | Use explicit --config flag for each command |
Post-Execution Reflection
After this skill completes, reflect before closing the task:
0. Locate yourself. — Find this SKILL.md's canonical path before editing. 1. What failed? — Fix the instruction that caused it. 2. What worked better than expected? — Promote to recommended practice. 3. What drifted? — Fix any script, reference, or dependency that no longer matches reality. 4. Log it. — Evolution-log entry with trigger, fix, and evidence.
Do NOT defer. The next invocation inherits whatever you leave behind.
Skill: Doppler Secret Validation
Doppler CLI Patterns
Common patterns for working with Doppler secrets management.
Basic Operations
List Projects
doppler projectsList Configs in Project
doppler configs --project PROJECT_NAMEList Secrets in Config
# Table view
doppler secrets --project PROJECT --config CONFIG
# Names only
doppler secrets --project PROJECT --config CONFIG --only-namesGet Single Secret
# Plain text (for scripting)
doppler secrets get SECRET_NAME --project PROJECT --config CONFIG --plain
# Table view (masked)
doppler secrets get SECRET_NAME --project PROJECT --config CONFIGSet Secret
doppler secrets set SECRET_NAME="value" --project PROJECT --config CONFIGNote: Doppler CLI doesn't support --note flag. Add notes via dashboard: 1. Go to https://dashboard.doppler.com 2. Navigate: PROJECT → CONFIG → SECRET_NAME 3. Click "Edit" → Add note
Environment Injection
Run Command with Secrets
# Doppler injects all secrets as environment variables
doppler run --project PROJECT --config CONFIG -- COMMANDExamples:
# Run Python script
doppler run --project claude-config --config prd -- python script.py
# Run with uv
doppler run --project claude-config --config prd -- uv run script.py
# Run shell command
doppler run --project claude-config --config prd -- bash -c 'echo $SECRET_NAME'Export Secret to Environment
/usr/bin/env bash << 'CONFIG_EOF'
# Export for current shell
export SECRET_NAME=$(doppler secrets get SECRET_NAME \
--project PROJECT --config CONFIG --plain)
# Use in commands
command --token $SECRET_NAME
CONFIG_EOFValidation Workflow
1. Add Secret
doppler secrets set PYPI_TOKEN="pypi-..." \
--project claude-config --config prd2. Verify Storage
# Check exists
doppler secrets --project claude-config --config prd | grep PYPI_TOKEN
# Retrieve value
doppler secrets get PYPI_TOKEN --project claude-config --config prd --plain3. Test Retrieval
/usr/bin/env bash << 'CONFIG_EOF_2'
TOKEN=$(doppler secrets get PYPI_TOKEN --project claude-config --config prd --plain)
echo "Length: ${#TOKEN}"
CONFIG_EOF_24. Test Environment Injection
/usr/bin/env bash << 'CONFIG_EOF_3'
doppler run --project claude-config --config prd -- \
bash -c 'echo "Token available: ${PYPI_TOKEN:0:20}..."'
CONFIG_EOF_3Tool Integration Patterns
uv Publish
/usr/bin/env bash << 'CONFIG_EOF_4'
# Method 1: Doppler auto-injects PYPI_TOKEN
doppler run --project claude-config --config prd -- uv publish
# Method 2: Manual export
export PYPI_TOKEN=$(doppler secrets get PYPI_TOKEN \
--project claude-config --config prd --plain)
uv publish --token $PYPI_TOKEN
CONFIG_EOF_4twine Upload
/usr/bin/env bash << 'CONFIG_EOF_5'
# Method 1: Doppler run (uses PYPI_TOKEN as password)
doppler run --project claude-config --config prd -- \
twine upload dist/* --username __token__
# Method 2: Manual export
export TWINE_PASSWORD=$(doppler secrets get PYPI_TOKEN \
--project claude-config --config prd --plain)
export TWINE_USERNAME=__token__
twine upload dist/*
CONFIG_EOF_5GitHub Actions
- name: Setup Doppler
uses: dopplerhq/secrets-fetch-action@v1.3.0
with:
doppler-token: ${{ secrets.DOPPLER_TOKEN }}
doppler-project: claude-config
doppler-config: prd
- name: Use Secret
run: uv publish
env:
PYPI_TOKEN: ${{ steps.doppler.outputs.PYPI_TOKEN }}Security Best Practices
1. Never Log Secrets
/usr/bin/env bash << 'DOPPLER_PATTERNS_SCRIPT_EOF'
# ✗ BAD: Logs secret
echo "Token: $PYPI_TOKEN"
# ✓ GOOD: Masks secret
echo "Token: ${PYPI_TOKEN:0:20}..."
DOPPLER_PATTERNS_SCRIPT_EOF2. Use --plain for Scripts Only
# ✗ BAD: Human-readable shows secret
doppler secrets get TOKEN --project foo --config bar
# ✓ GOOD: Plain text for piping to commands
doppler secrets get TOKEN --project foo --config bar --plain | command3. Prefer doppler run Over Export
/usr/bin/env bash << 'CONFIG_EOF_6'
# ✓ BEST: Scoped to single command
doppler run --project foo --config bar -- command
# ⚠ OK: Exported to shell session (risk if shell shared)
export TOKEN=$(doppler secrets get TOKEN --project foo --config bar --plain)
CONFIG_EOF_64. Use Separate Configs for Environments
project/
├── dev (development secrets)
├── stg (staging secrets)
└── prd (production secrets)Common Token Types
API Tokens
Format: Usually prefix-base64string
- PyPI:
pypi-AgEI...(179 chars) - GitHub:
ghp_...(40+ chars) - Quarto:
qpa_...(variable length)
Validation: Check prefix, length, and test authentication
Service Credentials
Format: Username/password pairs or JSON keys
- Store as separate secrets:
SERVICE_USER,SERVICE_PASS - Or as single JSON:
SERVICE_CREDENTIALS
Troubleshooting
Secret Not Found
# Check spelling
doppler secrets --project foo --config bar --only-names
# Check you're in right project/config
doppler whoamiPermission Denied
# Re-authenticate
doppler login
# Check project access
doppler projectsCommand Timeout
# Increase timeout (if supported by command)
timeout 30 doppler secrets get TOKEN --project foo --config bar --plainReference
- Official docs: https://docs.doppler.com/docs
- CLI reference: https://docs.doppler.com/docs/cli
- Install:
brew install dopplerhq/cli/doppler
Evolution Log
Convention: Reverse chronological order (newest on top, oldest at bottom). Prepend new entries.
---
2026-02-26: Initial Evolution Log
Status: Skill is in use and maintained. Track improvements here.
Purpose
This evolution log tracks updates to the skill. Each entry should note:
- What changed (content, structure, tooling)
- Why it changed (bug fix, feature request, best practice)
- Files affected
How to Use
1. When updating SKILL.md or references, add an entry here with the date 2. Keep entries reverse-chronological (newest first) 3. Link to ADRs or GitHub issues when relevant 4. Reference specific line changes when helpful
---
#!/usr/bin/env python3
# /// script
# dependencies = []
# ///
"""
Test API authentication using token from Doppler.
This is a template script - adapt the test_api_authentication() function
for your specific API.
Usage:
# Via Doppler (recommended)
doppler run --project PROJECT --config CONFIG -- uv run test_api_auth.py --secret SECRET_NAME --api-url API_URL
# Manual (for testing)
uv run test_api_auth.py --secret SECRET_NAME --api-url API_URL --token TOKEN
Example (PyPI):
doppler run --project claude-config --config prd -- \
uv run test_api_auth.py --secret PYPI_TOKEN --api-url https://upload.pypi.org/legacy/
"""
import argparse
import os
import subprocess
import sys
import urllib.error
import urllib.request
def get_token_from_env(secret_name: str) -> str | None:
"""Get token from environment (injected by Doppler)."""
return os.getenv(secret_name)
def get_token_from_doppler(project: str, config: str, secret_name: str) -> str | None:
"""Directly retrieve token from Doppler (fallback)."""
try:
result = subprocess.run(
['doppler', 'secrets', 'get', secret_name,
'--project', project, '--config', config, '--plain'],
capture_output=True,
text=True,
check=True,
timeout=10
)
return result.stdout.strip()
except Exception:
return None
def test_api_authentication(token: str, api_url: str) -> tuple[bool, str]:
"""
Test API authentication with token.
TEMPLATE: Customize this function for your specific API.
Args:
token: Authentication token
api_url: API endpoint to test
Returns:
(success: bool, message: str)
"""
try:
req = urllib.request.Request(api_url)
req.add_header('Authorization', f'Bearer {token}')
try:
with urllib.request.urlopen(req, timeout=10) as response:
return (True, f"API responded: {response.status} OK")
except urllib.error.HTTPError as e:
# Some APIs return error codes for GET on POST-only endpoints
# but still validate authentication
if e.code in [405, 404]: # Method Not Allowed / Not Found
return (True, f"Authentication successful (code {e.code} expected)")
elif e.code == 401:
return (False, "Authentication failed: 401 Unauthorized")
elif e.code == 403:
return (False, "Authentication failed: 403 Forbidden")
else:
return (True, f"API responded: {e.code} (may indicate valid auth)")
except urllib.error.URLError as e:
return (False, f"Network error: {e}")
except Exception as e:
return (False, f"Unexpected error: {e}")
def main():
parser = argparse.ArgumentParser(
description='Test API authentication using token from Doppler'
)
parser.add_argument('--secret', required=True, help='Secret name (e.g., PYPI_TOKEN)')
parser.add_argument('--api-url', required=True, help='API endpoint to test')
parser.add_argument('--token', help='Token value (for manual testing, not recommended)')
parser.add_argument('--project', help='Doppler project (if not using doppler run)')
parser.add_argument('--config', help='Doppler config (if not using doppler run)')
args = parser.parse_args()
print("=== API Authentication Test ===")
print(f"Secret: {args.secret}")
print(f"API URL: {args.api_url}\n")
# Get token
token = None
if args.token:
print("⚠ Using manually provided token")
token = args.token
else:
# Try environment first (doppler run injects it)
token = get_token_from_env(args.secret)
if token:
print("✓ Token retrieved from environment (via Doppler)")
elif args.project and args.config:
# Fallback to direct Doppler call
print("ℹ Retrieving token directly from Doppler...")
token = get_token_from_doppler(args.project, args.config, args.secret)
if token:
print("✓ Token retrieved from Doppler CLI")
if not token:
print("✗ No token available")
print("\nUsage:")
print(f" doppler run --project PROJECT --config CONFIG -- uv run {sys.argv[0]} --secret {args.secret} --api-url {args.api_url}")
sys.exit(1)
print(f"✓ Token: {token[:20]}...{token[-10:] if len(token) > 30 else ''}")
print(f"✓ Length: {len(token)} characters\n")
# Test authentication
print("Testing API authentication...")
success, message = test_api_authentication(token, args.api_url)
print(f" {message}\n")
if success:
print("🎉 Authentication successful!")
print(f"\nToken is valid for use with {args.api_url}")
sys.exit(0)
else:
print("✗ Authentication failed")
print("\nCheck:")
print(" - Token is correct and not expired")
print(" - API URL is correct")
print(" - Token has necessary permissions")
sys.exit(1)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# /// script
# dependencies = []
# ///
"""
Validate secret storage and retrieval from Doppler.
Usage:
uv run validate_secret.py --project PROJECT --config CONFIG --secret SECRET_NAME
Example:
uv run validate_secret.py --project claude-config --config prd --secret PYPI_TOKEN
"""
import argparse
import subprocess
import sys
def get_secret_from_doppler(project: str, config: str, secret_name: str) -> tuple[bool, str]:
"""
Retrieve secret from Doppler.
Returns:
(success: bool, value: str or error_message: str)
"""
try:
result = subprocess.run(
['doppler', 'secrets', 'get', secret_name,
'--project', project, '--config', config, '--plain'],
capture_output=True,
text=True,
check=True,
timeout=10
)
return (True, result.stdout.strip())
except subprocess.CalledProcessError as e:
return (False, f"Failed to retrieve secret: {e.stderr}")
except subprocess.TimeoutExpired:
return (False, "Doppler command timed out after 10 seconds")
except FileNotFoundError:
return (False, "Doppler CLI not found. Install via: brew install dopplerhq/cli/doppler")
def verify_secret_exists(project: str, config: str, secret_name: str) -> bool:
"""Check if secret exists in Doppler config."""
try:
result = subprocess.run(
['doppler', 'secrets', '--project', project, '--config', config, '--only-names'],
capture_output=True,
text=True,
check=True,
timeout=10
)
return secret_name in result.stdout.split('\n')
except subprocess.CalledProcessError as e:
print(f"[doppler-secret-validation] Failed to list secrets: {e.stderr or e}", file=sys.stderr)
return False
except subprocess.TimeoutExpired:
print("[doppler-secret-validation] Doppler command timed out", file=sys.stderr)
return False
except FileNotFoundError:
print("[doppler-secret-validation] Doppler CLI not found", file=sys.stderr)
return False
def test_env_injection(project: str, config: str, secret_name: str) -> bool:
"""Test that Doppler injects secret into environment."""
try:
result = subprocess.run(
['doppler', 'run', '--project', project, '--config', config, '--',
'python3', '-c', f'import os; v = os.getenv("{secret_name}"); print("OK" if v else "MISSING")'],
capture_output=True,
text=True,
check=True,
timeout=10
)
return 'OK' in result.stdout
except subprocess.CalledProcessError as e:
print(f"[doppler-secret-validation] Environment injection failed: {e.stderr or e}", file=sys.stderr)
return False
except subprocess.TimeoutExpired:
print("[doppler-secret-validation] Environment injection timed out", file=sys.stderr)
return False
except FileNotFoundError:
print("[doppler-secret-validation] Doppler CLI not found", file=sys.stderr)
return False
def main():
parser = argparse.ArgumentParser(
description='Validate secret storage and retrieval from Doppler'
)
parser.add_argument('--project', required=True, help='Doppler project name')
parser.add_argument('--config', required=True, help='Doppler config name (e.g., dev, prd)')
parser.add_argument('--secret', required=True, help='Secret name to validate')
parser.add_argument('--show-value', action='store_true',
help='Show secret value (security risk, use only for debugging)')
args = parser.parse_args()
print(f"=== Validating Secret: {args.secret} ===")
print(f"Project: {args.project}")
print(f"Config: {args.config}\n")
# Test 1: Check existence
print("1. Checking if secret exists...")
exists = verify_secret_exists(args.project, args.config, args.secret)
if exists:
print(f" ✓ Secret '{args.secret}' exists in {args.project}/{args.config}\n")
else:
print(f" ✗ Secret '{args.secret}' NOT found in {args.project}/{args.config}\n")
sys.exit(1)
# Test 2: Retrieve secret
print("2. Retrieving secret value...")
success, value = get_secret_from_doppler(args.project, args.config, args.secret)
if success:
if args.show_value:
print(f" ✓ Retrieved: {value}\n")
else:
print(f" ✓ Retrieved: {value[:20]}...{value[-10:] if len(value) > 30 else ''}")
print(f" ✓ Length: {len(value)} characters\n")
else:
print(f" ✗ {value}\n")
sys.exit(1)
# Test 3: Environment injection
print("3. Testing environment injection...")
injected = test_env_injection(args.project, args.config, args.secret)
if injected:
print(" ✓ Environment injection working\n")
else:
print(" ✗ Environment injection failed\n")
sys.exit(1)
# Summary
print("=== Validation Summary ===")
print("✓ Secret exists in Doppler")
print("✓ Secret retrieval working")
print("✓ Environment injection working")
print(f"\n🎉 Secret '{args.secret}' is fully operational!")
print("\nUsage:")
print(f" doppler run --project {args.project} --config {args.config} -- <command>")
if __name__ == '__main__':
main()