From d95251d880bf7146b29a3bfcda9121652543b48e Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Tue, 24 Mar 2026 13:06:56 -0700 Subject: [PATCH] Session log: 1Password skill setup, Lonestar MDM fix, credentials migration planning - Activated 1Password skill for Claude Code (extracted from .skill ZIP) - Resolved Lonestar Electrical MDM issue: ManageEngine was configured as third-party EMM in Google Workspace, causing persistent enrollment prompts on joser's personal phone - Scoped credentials.md migration to 1Password (op:// refs + MSP vaults) Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/commands/1password.md | 214 +++++++++++++++++ .../1password/references/integrations.md | 222 ++++++++++++++++++ .../1password/references/op_commands.md | 171 ++++++++++++++ .../1password/references/secret_references.md | 120 ++++++++++ .../skills/1password/scripts/check_setup.sh | 75 ++++++ .../skills/1password/scripts/env_from_op.sh | 142 +++++++++++ .../1password/scripts/launch-in-terminal.sh | 52 ++++ .../scripts/store-mcp-credentials.sh | 124 ++++++++++ .../skills/1password/scripts/store_secret.sh | 91 +++++++ session-logs/2026-03-24-session.md | 85 +++++++ 10 files changed, 1296 insertions(+) create mode 100644 .claude/commands/1password.md create mode 100644 .claude/skills/1password/references/integrations.md create mode 100644 .claude/skills/1password/references/op_commands.md create mode 100644 .claude/skills/1password/references/secret_references.md create mode 100755 .claude/skills/1password/scripts/check_setup.sh create mode 100755 .claude/skills/1password/scripts/env_from_op.sh create mode 100755 .claude/skills/1password/scripts/launch-in-terminal.sh create mode 100755 .claude/skills/1password/scripts/store-mcp-credentials.sh create mode 100755 .claude/skills/1password/scripts/store_secret.sh diff --git a/.claude/commands/1password.md b/.claude/commands/1password.md new file mode 100644 index 0000000..07a4bd6 --- /dev/null +++ b/.claude/commands/1password.md @@ -0,0 +1,214 @@ +--- +name: 1password +description: > + Integrate 1Password secrets management into Claude Code workflows. Use when the user wants to: + store API keys or credentials in 1Password, read secrets from 1Password into scripts or config, + set up .env files using 1Password secret references, rotate or update credentials, manage + developer secrets across projects, use 1Password service accounts for CI/CD, or integrate + 1Password with tools like Claude Desktop, n8n, Docker, Supabase, GitHub Actions, or Replit. + Triggers on phrases like "store in 1Password", "read from 1Password", "op://", "secret reference", + "manage API keys with 1Password", "1Password CLI", or any request involving the `op` command. +--- + +# 1Password Skill + +## ⚠️ Critical: Never Type Secrets Into Claude Code + +**Claude Code can see everything typed in its terminal and chat.** + +When a user needs to store a secret, ALWAYS use the Terminal launch pattern: +1. Generate a pre-filled script with known values already set +2. Use `launch-in-terminal.sh` to open it in Terminal.app +3. User types secrets in that window — Claude Code cannot see it +4. 1Password stores the secret, outputs `op://` references back to Claude + +```bash +# Claude generates the script, then launches it outside its own view: +bash scripts/launch-in-terminal.sh /tmp/setup-my-service.sh "Service Name Setup" +``` + +Never ask users to paste API keys, passwords, or tokens into: +- The Claude Code chat +- A Bash tool call visible in Claude Code +- Any file Claude Code writes before it's stored in 1Password + +--- + +## Setup Check + +Always verify the CLI is ready before any operation: + +```bash +bash scripts/check_setup.sh +``` + +If not installed: https://developer.1password.com/docs/cli/get-started/ +If not signed in: unlock the **1Password desktop app** (after Mac restart, the app must be unlocked before the CLI works) + +--- + +## Storing Secrets: The Terminal Launch Pattern + +When a user needs to store a new secret or credential: + +**Step 1 — Generate the script** (Claude does this, with known values pre-filled): + +```bash +cat > /tmp/setup-SERVICE.sh << 'EOF' +bash /path/to/store-mcp-credentials.sh \ + --vault Dev \ + --item "Service Name" \ + --set "url=https://known-url.com" \ + --set "env=production" \ + --secret "api_key" \ + --secret "webhook_secret" +EOF +``` + +**Step 2 — Launch in Terminal.app** (secrets stay out of Claude Code): + +```bash +bash scripts/launch-in-terminal.sh /tmp/setup-SERVICE.sh "Service Name Setup" +``` + +**Step 3 — Update config** (Claude uses the `op://` references from the output): + +```json +"SERVICE_API_KEY": "op://Dev/Service Name/api_key" +``` + +--- + +## Core Patterns + +### Read a secret + +```bash +op read "op://VaultName/ItemTitle/field_name" +export API_KEY=$(op read "op://Dev/Anthropic/api_key") +``` + +### Store a new secret + +```bash +# Basic +bash scripts/store_secret.sh --title "My API Key" --field api_key --value "sk-..." + +# With vault +bash scripts/store_secret.sh --title "My API Key" --vault Dev --field api_key --value "sk-..." + +# From environment variable +bash scripts/store_secret.sh --from-env ANTHROPIC_API_KEY --title "Anthropic" + +# Generate a secure credential +bash scripts/store_secret.sh --title "App Secret" --field secret --generate --length 32 +``` + +### Update an existing secret + +```bash +bash scripts/store_secret.sh --update --title "My API Key" --field api_key --value "new-value" +# Or directly: +op item edit "My API Key" api_key[password]=new-value +``` + +### Generate a .env from 1Password + +```bash +# Interactive — lists items, choose one +bash scripts/env_from_op.sh + +# From a specific item (dry run preview) +bash scripts/env_from_op.sh --item "Project Credentials" --dry-run + +# Write .env.tpl (secret references — safe to commit) +bash scripts/env_from_op.sh --item "Project Credentials" --output .env.tpl + +# Write .env with resolved real values (DO NOT commit) +bash scripts/env_from_op.sh --item "Project Credentials" --resolve --output .env +``` + +--- + +## Secret References (op://) + +The safest pattern — store `op://` references in config files instead of real values. + +> **Privacy note:** `op://` references reveal vault names, item names, and field names. +> Safe to commit to **private repos**. For public repos, check that your vault/item naming +> doesn't expose sensitive structure (client names, internal service names, etc.). + +``` +op://VaultName/ItemTitle/field_name +``` + +```bash +# .env.tpl (commit this file) +ANTHROPIC_API_KEY=op://Dev/Anthropic/api_key +N8N_API_KEY=op://Dev/n8n/api_key +SUPABASE_SERVICE_KEY=op://Dev/Supabase/service_key + +# ✅ Inject at runtime — secrets stay in subprocess, never in shell history +op run --env-file=.env.tpl -- your-command + +# ⚠️ Avoid sourcing into current shell — unsafe if values contain $(...) or backticks +# source <(op run --env-file=.env.tpl -- env) ← skip this pattern +``` + +For full syntax and edge cases: [references/secret_references.md](references/secret_references.md) + +--- + +## Integration Guides + +Read [references/integrations.md](references/integrations.md) for patterns with: + +- **Claude Desktop** — MCP server config using `op run` +- **n8n** — Environment injection at startup, credential push via API +- **Docker / Docker Compose** — `op run -- docker compose up` +- **GitHub Actions** — `1password/load-secrets-action` +- **Python scripts** — subprocess + 1Password SDK +- **Supabase** — Storing and retrieving project credentials +- **Replit** — Local dev → Replit Secrets bridge +- **Rotation workflow** — Update in service → update in 1Password → re-inject + +--- + +## Common CLI Commands + +Full reference: [references/op_commands.md](references/op_commands.md) + +```bash +op item list # List all items +op item list --vault Dev # Filter by vault +op item get "Item Title" # View item details +op item get "Item Title" --format json # JSON output +op vault list # List vaults +op whoami # Check auth status +op account list # List accounts +``` + +--- + +## CI/CD: Service Accounts + +For non-interactive environments (GitHub Actions, Docker, n8n server): + +```bash +export OP_SERVICE_ACCOUNT_TOKEN="ops_eyJ..." +op read "op://Dev/MyApp/api_key" # works without signin prompt +``` + +Create service accounts: 1Password UI → Settings → Developer → Service Accounts. +Grant vault access only to what the service needs. + +--- + +## Security Rules + +1. **Never hardcode secrets** — always use `op://` references or runtime injection +2. **Commit `.env.tpl`** to private repos only — it exposes vault/item structure, not values +3. **Never commit `.env`** (real values) — add it to `.gitignore` immediately: `echo ".env" >> .gitignore` +4. **Use vaults to scope access** — separate vault per project or team +5. **Rotate on exposure** — use `store_secret.sh --update` then re-inject everywhere +6. **Service accounts for CI/CD** — never use personal account tokens in automation diff --git a/.claude/skills/1password/references/integrations.md b/.claude/skills/1password/references/integrations.md new file mode 100644 index 0000000..4ae64a7 --- /dev/null +++ b/.claude/skills/1password/references/integrations.md @@ -0,0 +1,222 @@ +# 1Password Integration Patterns + +Common patterns for integrating 1Password with developer tools and AI workflows. + +## Claude Code / Claude Desktop + +### Claude Desktop MCP Config + +Store API keys securely and reference them in `claude_desktop_config.json`: + +```bash +# Store the key +op item create --category API_CREDENTIAL --title "My MCP Server" \ + --vault Dev api_key[password]=your-key-here + +# Get the secret reference +# op://Dev/My MCP Server/api_key +``` + +```json +{ + "mcpServers": { + "my-server": { + "command": "op", + "args": ["run", "--", "node", "/path/to/server.js"], + "env": { + "API_KEY": "op://Dev/My MCP Server/api_key" + } + } + } +} +``` + +### Claude Code Shell Environment + +```bash +# .env.tpl (safe to commit — no real secrets) +ANTHROPIC_API_KEY=op://Dev/Anthropic/api_key +OPENAI_API_KEY=op://Dev/OpenAI/api_key + +# ✅ Wrap claude with op run — secrets injected into subprocess only +op run --env-file=.env.tpl -- claude + +# ✅ Or export individually for interactive shell use +export ANTHROPIC_API_KEY=$(op read "op://Dev/Anthropic/api_key") +claude +``` + +### In CLAUDE.md (project secrets reference) + +```markdown +## Secrets Setup +Secrets are managed via 1Password. Run before working: +```bash +op run --env-file=.env.tpl -- claude +``` +Do NOT commit `.env` — commit `.env.tpl` only. +``` + +## n8n + +### Environment Injection at Startup + +```bash +# n8n.env.tpl (commit this) +N8N_ENCRYPTION_KEY=op://Dev/n8n/encryption_key +DB_POSTGRESDB_PASSWORD=op://Dev/n8n-postgres/password +N8N_BASIC_AUTH_PASSWORD=op://Dev/n8n/basic_auth_password + +# docker-compose.yml startup +op run --env-file=n8n.env.tpl -- docker compose up -d n8n +``` + +### n8n Credential Storage via API + +Use n8n's credential API to push secrets from 1Password into n8n: + +```bash +# Get secret from 1Password +API_KEY=$(op read "op://Dev/Some Service/api_key") + +# Push to n8n credential (HTTP Request) +curl -s -X POST "https://n8n.example.com/api/v1/credentials" \ + -H "X-N8N-API-KEY: $(op read 'op://Dev/n8n/api_key')" \ + -H "Content-Type: application/json" \ + -d "{\"name\": \"Service Credential\", \"type\": \"httpHeaderAuth\", \"data\": {\"name\": \"Authorization\", \"value\": \"Bearer $API_KEY\"}}" +``` + +## Docker / Docker Compose + +```yaml +# docker-compose.yml +services: + app: + image: myapp:latest + environment: + DATABASE_URL: ${DATABASE_URL} + API_KEY: ${API_KEY} +``` + +```bash +# .env.tpl +DATABASE_URL=op://Dev/Postgres/connection_string +API_KEY=op://Dev/MyApp/api_key + +# Start with injection +op run --env-file=.env.tpl -- docker compose up +``` + +## Python Scripts + +```python +import subprocess + +def get_secret(reference: str) -> str: + """Read a secret from 1Password using a secret reference.""" + result = subprocess.run( + ["op", "read", reference], + capture_output=True, text=True, check=True + ) + return result.stdout.strip() + +# Usage +api_key = get_secret("op://Dev/Anthropic/api_key") +``` + +Or using the 1Password Python SDK (if available): +```bash +pip install onepassword-sdk +``` + +```python +import asyncio +import onepassword + +async def main(): + client = await onepassword.Client.authenticate( + auth=os.environ["OP_SERVICE_ACCOUNT_TOKEN"], + integration_name="My Script", + integration_version="1.0.0", + ) + secret = await client.secrets.resolve("op://Dev/Anthropic/api_key") +``` + +## GitHub Actions / CI + +```yaml +# .github/workflows/deploy.yml +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: 1password/load-secrets-action@v2 + with: + export-env: true + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + ANTHROPIC_API_KEY: op://Dev/Anthropic/api_key + DEPLOY_KEY: op://Dev/Deploy/private_key + + - run: deploy-script.sh # ANTHROPIC_API_KEY is available +``` + +## Shell / .zshrc Auto-Load + +```bash +# ~/.zshrc +# Auto-load common dev secrets on shell start (optional — only if you trust your machine) +load_dev_secrets() { + if command -v op &>/dev/null && op whoami &>/dev/null 2>&1; then + source <(op run --env-file=~/.config/dev.env.tpl -- env 2>/dev/null) && \ + echo "✅ Dev secrets loaded from 1Password" + fi +} + +# Call explicitly when needed: +alias load-secrets='load_dev_secrets' +``` + +## Supabase + +```bash +# Store Supabase credentials +op item create --category API_CREDENTIAL --title "Supabase - My Project" \ + --vault Dev \ + url[text]=https://myproject.supabase.co \ + anon_key[password]=eyJ... \ + service_key[password]=eyJ... + +# Use in scripts +SUPABASE_URL=$(op read "op://Dev/Supabase - My Project/url") +SUPABASE_KEY=$(op read "op://Dev/Supabase - My Project/service_key") +``` + +## Replit + +Replit has its own Secrets manager, but for local dev before deploying: + +```bash +# Generate a .env from 1Password, then paste values into Replit Secrets UI +op run --env-file=.env.tpl -- env | grep -E "^(ANTHROPIC|SUPABASE|N8N)" +# Copy output values → paste into Replit Secrets one by one +``` + +## Rotation Workflow + +When rotating a credential: + +```bash +# 1. Update in the service (get new key) +NEW_KEY="new-key-from-service" + +# 2. Update in 1Password +op item edit "Service Name" api_key[password]="$NEW_KEY" + +# 3. Verify +op read "op://Dev/Service Name/api_key" + +# 4. Re-inject wherever used +source <(op run --env-file=.env.tpl -- env) +# Or restart services that use the key +``` diff --git a/.claude/skills/1password/references/op_commands.md b/.claude/skills/1password/references/op_commands.md new file mode 100644 index 0000000..3122731 --- /dev/null +++ b/.claude/skills/1password/references/op_commands.md @@ -0,0 +1,171 @@ +# 1Password CLI (op) Command Reference + +## Authentication + +```bash +# Sign in (interactive) +op signin + +# Sign in to specific account +op signin --account team-name.1password.com + +# Check who you're signed in as +op whoami + +# List accounts +op account list + +# Service account (CI/CD — set env var, no signin needed) +export OP_SERVICE_ACCOUNT_TOKEN="your-token" +``` + +## Items + +```bash +# List items +op item list +op item list --vault Dev +op item list --categories API_CREDENTIAL + +# Get item details +op item get "Item Title" +op item get "Item Title" --vault Dev +op item get "Item Title" --format json + +# Get a specific field +op item get "Item Title" --fields api_key +op item get "Item Title" --fields label=api_key + +# Read using secret reference (most common) +op read "op://Dev/Item Title/api_key" + +# Create item +op item create --category API_CREDENTIAL --title "My API Key" api_key[password]=sk-abc123 +op item create --category LOGIN --title "Service Account" --vault Dev \ + username[text]=myuser password[password]=mypass + +# Edit/update item +op item edit "Item Title" api_key[password]=new-value +op item edit "Item Title" --vault Dev new_field[text]=value + +# Delete item +op item delete "Item Title" +op item delete "Item Title" --vault Dev + +# Move item to different vault +op item move "Item Title" --current-vault Dev --destination-vault Personal +``` + +## Vaults + +```bash +# List vaults +op vault list +op vault list --format json + +# Create vault +op vault create "New Vault" + +# Get vault details +op vault get "Vault Name" +``` + +## Secrets Injection + +```bash +# Run command with secrets from .env template (RECOMMENDED) +op run --env-file=.env.tpl -- your-command arg1 arg2 + +# Inject into Docker +op run --env-file=.env.tpl -- docker compose up + +# Inject a single reference via env var (op run picks up op:// values automatically) +export API_KEY="op://Dev/MyApp/api_key" +op run -- node app.js # API_KEY is resolved at runtime + +# ⚠️ AVOID: sourcing op run output into the current shell +# source <(op run --env-file=.env.tpl -- env) ← UNSAFE +# If secret values contain $(...) or backticks, they execute as shell code. +# Use 'op run -- your-command' instead (secrets stay in subprocess only). +``` + +## Password Generation + +```bash +# Generate at item creation time (no standalone command) +op item create --category PASSWORD --title "Generated Secret" \ + --generate-password='letters,digits,symbols,32' + +# Generate with custom recipe +op item create --category LOGIN --title "My Login" \ + --generate-password='letters,digits,20' + +# Or use openssl for scripted generation +openssl rand -base64 32 | tr -d '=+/' +``` + +## Document / File Management + +```bash +# Store a file +op document create ./private-key.pem --title "SSH Private Key" --vault Dev + +# Get a file +op document get "SSH Private Key" --output ./private-key.pem + +# List documents +op document list +``` + +## Service Accounts (CI/CD) + +```bash +# Create service account (in 1Password UI: Settings → Developer → Service Accounts) +# Then set token as env var: +export OP_SERVICE_ACCOUNT_TOKEN="ops_eyJ..." + +# No signin needed — op commands work automatically +op item list # works with service account token +op read "op://vault/item/field" +``` + +## Connect (Self-hosted, advanced) + +```bash +# For teams running 1Password Connect server +export OP_CONNECT_HOST="https://your-connect-server" +export OP_CONNECT_TOKEN="your-connect-token" + +# Then op commands use Connect instead of 1Password.com +op item get "Item Title" +``` + +## Output Formats + +Valid values: `json` or `human-readable` (default). + +```bash +op item list --format=json # Machine-readable JSON +op item get "Item" --format=json # Full item JSON +op item list # Human-readable (default) +op vault list --format=json # Vaults as JSON +``` + +## Useful Patterns + +```bash +# Find item by field value (search) +op item list --format=json | \ + python3 -c "import sys,json; [print(i['title']) for i in json.load(sys.stdin)]" + +# Export all items in a vault to JSON (backup) +op item list --vault Dev --format=json | \ + python3 -c "import sys,json; ids=[i['id'] for i in json.load(sys.stdin)]" +# (then loop to get each) + +# Check if a specific item exists +op item get "My Item" &>/dev/null && echo "exists" || echo "not found" + +# Get item ID (for scripting) +op item get "My Item" --format=json | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" +``` diff --git a/.claude/skills/1password/references/secret_references.md b/.claude/skills/1password/references/secret_references.md new file mode 100644 index 0000000..72db38d --- /dev/null +++ b/.claude/skills/1password/references/secret_references.md @@ -0,0 +1,120 @@ +# 1Password Secret References + +Secret references are the safest way to use secrets — they point to 1Password without exposing actual values in code or config files. + +## Syntax + +``` +op://vault/item/field +op://vault/item/section/field +``` + +**Examples:** +```bash +op://Dev/Anthropic/api_key +op://Personal/AWS/access_key_id +op://Dev/Supabase/section/service_key +``` + +## Reading a Secret Reference + +```bash +# Single secret +op read "op://Dev/Anthropic/api_key" + +# Into a variable +export ANTHROPIC_API_KEY=$(op read "op://Dev/Anthropic/api_key") + +# Multiple secrets via op run +op run --env-file=.env.tpl -- your-command +``` + +## .env Template Files + +Store references in a `.env.tpl` file (safe to commit to **private** repos): + +> **Privacy note:** `.env.tpl` contains your vault names, item names, and field names — +> e.g. `op://Dev/Anthropic/api_key`. This reveals the structure of your 1Password vault +> to anyone who can read the file. For **private repos**, this is fine. For **public repos**, +> consider whether your vault/item naming reveals anything sensitive (client names, internal +> service names, etc.). Real secret values are never exposed — only the structure. + +```bash +# .env.tpl — commit this +ANTHROPIC_API_KEY=op://Dev/Anthropic/api_key +N8N_API_KEY=op://Dev/n8n/api_key +SUPABASE_SERVICE_KEY=op://Dev/Supabase/service_key +NOTION_TOKEN=op://Dev/Notion/api_token +``` + +Then inject at runtime: +```bash +# ✅ RECOMMENDED — run your command with secrets injected into subprocess only +op run --env-file=.env.tpl -- npm start +op run --env-file=.env.tpl -- node server.js +op run --env-file=.env.tpl -- docker compose up + +# ✅ OK — read a single secret into a variable for immediate use +export ANTHROPIC_API_KEY=$(op read "op://Dev/Anthropic/api_key") + +# ⚠️ AVOID — sourcing op run output exposes secrets in current shell +# and is unsafe if any secret value contains shell metacharacters like $(...): +# source <(op run --env-file=.env.tpl -- env) ← DON'T DO THIS + +# ⚠️ AVOID — writing resolved secrets to disk (don't commit .env) +# op run --env-file=.env.tpl -- env > .env ← only if truly necessary +``` + +## In Config Files + +Claude Desktop (`claude_desktop_config.json`): +```json +{ + "mcpServers": { + "my-server": { + "command": "op", + "args": ["run", "--", "node", "server.js"], + "env": { + "API_KEY": "op://Dev/MyServer/api_key" + } + } + } +} +``` + +Docker Compose: +```yaml +services: + app: + image: myapp + environment: + - DATABASE_URL=op://Dev/Postgres/connection_string +``` +Run with: `op run -- docker compose up` + +n8n (environment injection): +```bash +# In your n8n startup script +op run --env-file=n8n.env.tpl -- docker compose up n8n +``` + +## Finding Field Names + +```bash +# List all fields in an item +op item get "Item Name" --format=json | \ + python3 -c "import sys,json; [print(f['label']) for f in json.load(sys.stdin)['fields'] if f.get('value')]" + +# Or view interactively +op item get "Item Name" +``` + +## Common Field Names by Category + +| Category | Common Fields | +|----------|---------------| +| API_CREDENTIAL | `api_key`, `credential`, `token` | +| LOGIN | `username`, `password` | +| DATABASE | `connection_string`, `host`, `port`, `username`, `password` | +| SECURE_NOTE | `notesPlain` | +| SERVER | `hostname`, `port`, `username`, `password` | diff --git a/.claude/skills/1password/scripts/check_setup.sh b/.claude/skills/1password/scripts/check_setup.sh new file mode 100755 index 0000000..3b0a37f --- /dev/null +++ b/.claude/skills/1password/scripts/check_setup.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# check_setup.sh — Verify 1Password CLI is installed and authenticated +# Usage: bash check_setup.sh + +set -euo pipefail + +PASS=0 +FAIL=0 + +check() { + local label="$1" + local cmd="$2" + if eval "$cmd" &>/dev/null; then + echo " ✅ $label" + ((PASS++)) || true + else + echo " ❌ $label" + ((FAIL++)) || true + fi +} + +echo "=== 1Password CLI Setup Check ===" +echo "" + +# 1. CLI installed +check "op CLI installed" "command -v op" + +# 2. Version +if command -v op &>/dev/null; then + echo " ℹ️ Version: $(op --version)" +fi + +echo "" +echo "--- Authentication ---" + +# 3. Signed in +check "Signed in to 1Password" "op account list 2>/dev/null | grep -q '.'" + +# 4. Can list vaults +check "Can list vaults" "op vault list &>/dev/null" + +# Show accounts if authenticated +if op account list &>/dev/null 2>&1; then + echo "" + echo " Accounts:" + op account list 2>/dev/null | tail -n +2 | while read -r line; do + echo " • $line" + done + + echo "" + echo " Vaults:" + op vault list --format=json 2>/dev/null | \ + python3 -c "import sys,json; [print(f' • {v[\"name\"]} ({v[\"id\"]})') for v in json.load(sys.stdin)]" 2>/dev/null || true +fi + +echo "" +echo "--- Environment ---" + +# 5. OP_SERVICE_ACCOUNT_TOKEN (CI/CD pattern) +if [[ -n "${OP_SERVICE_ACCOUNT_TOKEN:-}" ]]; then + echo " ✅ OP_SERVICE_ACCOUNT_TOKEN is set (service account mode)" +else + echo " ℹ️ OP_SERVICE_ACCOUNT_TOKEN not set (interactive/desktop app mode)" +fi + +echo "" +echo "===================================" +if [[ $FAIL -eq 0 ]]; then + echo "✅ All checks passed. 1Password CLI is ready." +else + echo "⚠️ $FAIL check(s) failed. See above." + echo "" + echo "Install: https://developer.1password.com/docs/cli/get-started/" + echo "Sign in: op signin" +fi diff --git a/.claude/skills/1password/scripts/env_from_op.sh b/.claude/skills/1password/scripts/env_from_op.sh new file mode 100755 index 0000000..17e1d4e --- /dev/null +++ b/.claude/skills/1password/scripts/env_from_op.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# env_from_op.sh — Generate a .env file from 1Password items +# +# Usage: +# bash env_from_op.sh # Interactive: prompts for vault + items +# bash env_from_op.sh --vault Dev # Use specific vault +# bash env_from_op.sh --item "My Project" # Export all fields from one item +# bash env_from_op.sh --output .env # Write to file (default: .env) +# bash env_from_op.sh --dry-run # Print without writing +# +# Output format: +# FIELD_NAME=op://Vault/Item/field # Secret references (safest) +# FIELD_NAME=actual_value # Resolved values (with --resolve) + +set -euo pipefail + +VAULT="" +ITEM="" +OUTPUT=".env" +DRY_RUN=false +RESOLVE=false + +# Parse args +while [[ $# -gt 0 ]]; do + case $1 in + --vault) VAULT="$2"; shift 2 ;; + --item) ITEM="$2"; shift 2 ;; + --output) OUTPUT="$2"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + --resolve) RESOLVE=true; shift ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +# Check op is available +if ! command -v op &>/dev/null; then + echo "❌ 1Password CLI (op) not found. Install: https://developer.1password.com/docs/cli/get-started/" + exit 1 +fi + +# If no item specified, list items and prompt +if [[ -z "$ITEM" ]]; then + echo "Available items in vault '${VAULT:-all vaults}':" + if [[ -n "$VAULT" ]]; then + op item list --vault "$VAULT" --format=json | \ + python3 -c "import sys,json; [print(f' {i[\"title\"]}') for i in json.load(sys.stdin)]" + else + op item list --format=json | \ + python3 -c "import sys,json; [print(f' [{i[\"vault\"][\"name\"]}] {i[\"title\"]}') for i in json.load(sys.stdin)]" + fi + echo "" + read -rp "Enter item title: " ITEM +fi + +echo "Fetching '${ITEM}' from 1Password..." + +# Get item as JSON +if [[ -n "$VAULT" ]]; then + ITEM_JSON=$(op item get "$ITEM" --vault "$VAULT" --format=json) +else + ITEM_JSON=$(op item get "$ITEM" --format=json) +fi + +VAULT_NAME=$(echo "$ITEM_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['vault']['name'])") +ITEM_TITLE=$(echo "$ITEM_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['title'])") + +# Build .env content +ENV_CONTENT=$(echo "$ITEM_JSON" | python3 - <<'PYEOF' +import sys, json, re + +data = json.load(sys.stdin) +vault = data['vault']['name'] +title = data['title'] +lines = [] + +SKIP_LABELS = {'username', 'password', 'notesPlain', 'notes'} +SKIP_TYPES = {'CONCEALED'} if False else set() # resolved mode: don't skip + +for field in data.get('fields', []): + label = field.get('label', '') + value = field.get('value', '') + field_id = field.get('id', '') + ftype = field.get('type', '') + + # Skip empty, metadata, or UI-only fields + if not value or not label: + continue + if label.lower() in {'username', 'notesplain', 'notes', 'password'} and ftype not in {'CONCEALED', 'URL'}: + continue + + # Convert label to ENV_VAR format + env_key = re.sub(r'[^A-Z0-9_]', '_', label.upper().replace(' ', '_').replace('-', '_')) + env_key = re.sub(r'_+', '_', env_key).strip('_') + + # Use secret reference (safer than raw value) + ref = f"op://{vault}/{title}/{label}" + lines.append(f"{env_key}={ref}") + +print('\n'.join(lines)) +PYEOF +) + +# Handle resolve flag — replace refs with real values +if $RESOLVE; then + echo "⚠️ Writing resolved values (actual secrets). Handle carefully." + FINAL_CONTENT="" + while IFS= read -r line; do + if [[ "$line" =~ ^([A-Z_]+)=(op://.+)$ ]]; then + key="${BASH_REMATCH[1]}" + ref="${BASH_REMATCH[2]}" + value=$(op read "$ref" 2>/dev/null || echo "ERROR_READING") + FINAL_CONTENT+="${key}=${value}"$'\n' + else + FINAL_CONTENT+="$line"$'\n' + fi + done <<< "$ENV_CONTENT" + ENV_CONTENT="$FINAL_CONTENT" +fi + +# Header +HEADER="# Generated from 1Password: ${VAULT_NAME}/${ITEM_TITLE} +# Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ) +# Load with: op run --env-file=.env -- +# or: eval \$(op run --env-file=.env -- env | grep KEY) + +" + +FULL_CONTENT="${HEADER}${ENV_CONTENT}" + +if $DRY_RUN; then + echo "" + echo "--- .env preview ---" + echo "$FULL_CONTENT" + echo "--- end ---" +else + echo "$FULL_CONTENT" > "$OUTPUT" + echo "✅ Written to $OUTPUT (${#ENV_CONTENT} chars, $(echo "$ENV_CONTENT" | grep -c '=' || true) vars)" + echo "" + echo "To use:" + echo " op run --env-file=$OUTPUT -- your-command" + echo " source <(op run --env-file=$OUTPUT -- env)" +fi diff --git a/.claude/skills/1password/scripts/launch-in-terminal.sh b/.claude/skills/1password/scripts/launch-in-terminal.sh new file mode 100755 index 0000000..ea5c944 --- /dev/null +++ b/.claude/skills/1password/scripts/launch-in-terminal.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# launch-in-terminal.sh — Open a script in a NEW Terminal.app window +# +# This is how the 1Password skill keeps secrets OUT of Claude Code. +# Claude generates the script, then calls this launcher. +# The script runs in Terminal.app — Claude never sees what you type. +# +# Usage: +# bash launch-in-terminal.sh /path/to/script.sh +# bash launch-in-terminal.sh /path/to/script.sh "window title" + +set -euo pipefail + +SCRIPT_PATH="${1:-}" +TITLE="${2:-1Password Setup}" + +if [[ -z "$SCRIPT_PATH" ]]; then + echo "Usage: bash launch-in-terminal.sh /path/to/script.sh" + exit 1 +fi + +if [[ ! -f "$SCRIPT_PATH" ]]; then + echo "❌ Script not found: $SCRIPT_PATH" + exit 1 +fi + +chmod +x "$SCRIPT_PATH" + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Opening Terminal.app to collect secrets" +echo " Script: $SCRIPT_PATH" +echo "" +echo " ⚠️ Type your secrets in the Terminal" +echo " window that is about to open." +echo " Claude Code cannot see that window." +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +osascript <