| name | op-env |
| description | .envファイルのシークレットを1Password CLIで管理する(平文.envの保存・Git流出対策。AIからの秘匿は対象外)。migrate/run/sync/addをサポート。 |
| disable-model-invocation | true |
| allowed-tools | Bash, Read |
| argument-hint | [migrate|run|sync|add] [options] |
op-env
.envファイルのシークレットを1Password CLI (op) で管理する。
セキュリティに関する重要な注意事項
op-envは平文.envの保存・Git流出・チーム配布のリスクを下げるツールです。
op run --env-fileは実行プロセスの環境変数にシークレットを展開するため、
AIエージェントや実行プロセスからシークレットを隠すものではありません。
このスキルがリスクを低減する脅威
- 平文.envファイルのGitリポジトリへの誤コミットリスク
- 平文シークレットのローカルファイルシステム上への長期残存
- シークレットの分散管理によるローテーション困難・チーム配布の非効率
このスキルが解決しない脅威
- AIエージェントからのシークレット読み取り:
op runは環境変数に展開するため、同一プロセス内からは読み取り可能
- 実行環境の侵害: 環境変数は
/proc/<pid>/environやプロセスリストから読める場合がある
- 漏洩時の被害最小化: キーのスコープ制限・レート制限・決済上限はAPI提供者側で設定する必要がある
AI開発環境での推奨事項
- 開発専用キーのみ使用: 本番用・高権限・決済系のキーはAI実行環境に入れない
- 最小権限の原則: API キーは必要最小限のスコープ・権限で発行する
- 1Password側の最小権限化: 個人の広権限セッションではなく、必要なvaultのみにアクセスできるService Accountで認証する
- 短命トークンの活用: 可能な限りSTS/OIDC/OAuthで短命(1〜15分)のトークンを使う
- 利用量制限: API側でレート制限・1日上限・決済上限を設定する
- 環境分離: dev/staging/prodで鍵・vault・請求境界を完全に分離する
- 監査ログ: API利用ログを有効化し、異常検知を設定する
- 漏洩対応: 漏洩を検知した場合は即時ローテーション・失効を行う
- バックアップの即時削除: migrateで生成される.env.bakには平文が含まれるため、動作確認後に速やかに削除する
- CLI引数への平文注意: migrate/sync --push/addはop CLI引数に平文を渡す。プロセスリストから見える可能性がある
Claude Codeのsettings.jsonによる事故防止
~/.claude/settings.jsonのpermissions.denyで、AIエージェントによるシークレットファイルの読み取りや環境変数の表示を制限できます。これは強制的なセキュリティ境界ではなく事故防止のガードレールです(ユーザー自身が変更可能なため)。強制力が必要な場合はmanaged settingsを使用してください。
設定例:
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(**/.env)",
"Read(**/.env.*)",
"Read(**/secrets/**)",
"Read(**/*.pem)",
"Bash(env:*)",
"Bash(printenv:*)"
]
}
}
注意: Bashのdenyルールは*ワイルドカード付きのコマンド文字列マッチであり、cat .env等の回避手段を完全には防げません。より厳密な制御にはPreToolUse hooksやsandbox併用を検討してください。
プロジェクトに合わせたdenyルールの自動生成には /secret-boundary baseline を使用してください。
概要
migrate <envfile> → .envの値を1Passwordに保存し、op://参照に置換
run <envfile> -- <cmd> → op run --env-file でコマンド実行
sync <envfile> → .envと1Passwordの差分検出・同期
add <key> <value> → 新規シークレットを1Passwordに追加し.envに参照を追記
前提チェック
スキル実行前に以下を確認:
if ! command -v op >/dev/null 2>&1; then
echo "Error: 1Password CLI (op) がインストールされていません"
echo "Install: https://developer.1password.com/docs/cli/get-started/"
exit 1
fi
if ! op account list >/dev/null 2>&1; then
echo "Error: 1Passwordにサインインしてください"
echo "Run: op signin (1Password 8+) or eval \$(op signin) (旧バージョン)"
exit 1
fi
共通設定
VAULT="${OP_ENV_VAULT:-Development}"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
PROJECT_NAME=$(basename "$(git rev-parse --show-toplevel)")
else
PROJECT_NAME=$(basename "$PWD")
fi
ITEM_NAME="${OP_ENV_ITEM:-${PROJECT_NAME}}"
コマンド
migrate
既存の.envファイルを1Passwordに移行し、op://参照に置換する。
引数: .envファイルパス(デフォルト: .env)
ENV_FILE="${1:-.env}"
if [ ! -f "$ENV_FILE" ]; then
echo "Error: ${ENV_FILE} が見つかりません"
exit 1
fi
BACKUP_FILE="${ENV_FILE}.bak.$(date +%Y%m%d%H%M%S)"
cp "$ENV_FILE" "$BACKUP_FILE" \
|| { echo "Error: バックアップの作成に失敗しました"; exit 1; }
echo "バックアップ: ${BACKUP_FILE}"
EXISTING_ITEM=$(op item get "$ITEM_NAME" --vault="$VAULT" --format=json 2>/dev/null)
FIELDS=()
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue
[[ "$line" == *"op://"* ]] && continue
[[ "$line" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]] || continue
KEY="${line%%=*}"
VALUE="${line#*=}"
VALUE=$(echo "$VALUE" | sed "s/^['\"]//; s/['\"]$//; s/^[[:space:]]*//; s/[[:space:]]*$//")
VALUE=$(echo "$VALUE" | sed 's/[[:space:]]#.*$//')
[ -z "$KEY" ] && continue
FIELDS+=("${KEY}[password]=${VALUE}")
done < "$ENV_FILE"
if [ ${#FIELDS[@]} -eq 0 ]; then
echo "移行対象のシークレットがありません"
exit 0
fi
echo "=== 移行対象 ==="
echo "Vault: ${VAULT}"
echo "Item: ${ITEM_NAME}"
echo "Fields:"
for F in "${FIELDS[@]}"; do
echo " - ${F%%=*}"
done
echo ""
echo "この内容で1Passwordに保存します。ユーザーに確認してください。"
echo "確認が取れるまで以降の処理を実行しないこと。"
exit 0
確認後、以下を実行:
if [ -n "$EXISTING_ITEM" ]; then
op item edit "$ITEM_NAME" --vault="$VAULT" "${FIELDS[@]}" \
|| { echo "Error: 1Passwordアイテムの更新に失敗しました。バックアップ: ${BACKUP_FILE}"; exit 1; }
else
op item create --category=login --title="$ITEM_NAME" --vault="$VAULT" "${FIELDS[@]}" \
|| { echo "Error: 1Passwordアイテムの作成に失敗しました。バックアップ: ${BACKUP_FILE}"; exit 1; }
fi
TEMP_FILE=$(mktemp)
while IFS= read -r line; do
if [[ -z "$line" || "$line" == \#* || "$line" == *"op://"* ]]; then
echo "$line"
continue
fi
KEY="${line%%=*}"
[ -z "$KEY" ] && { echo "$line"; continue; }
echo "${KEY}=op://${VAULT}/${ITEM_NAME}/${KEY}"
done < "$ENV_FILE" > "$TEMP_FILE"
mv "$TEMP_FILE" "$ENV_FILE"
echo "=== 移行完了 ==="
echo "元ファイル: ${BACKUP_FILE}"
echo "更新後: ${ENV_FILE}"
echo ""
echo "動作確認: op run --env-file=${ENV_FILE} -- <your-command>"
echo ""
echo "重要: バックアップファイル ${BACKUP_FILE} には平文シークレットが含まれます。"
echo "動作確認後、速やかに削除してください: rm ${BACKUP_FILE}"
if [ -f .gitignore ]; then
if ! grep -qE '^\*\.env\.bak' .gitignore; then
echo '*.env.bak.*' >> .gitignore
echo ".gitignoreに *.env.bak.* を追加しました"
fi
fi
run
op run --env-fileでコマンドを実行する。
引数: .envファイルパス + -- + 実行コマンド
if [ $# -eq 0 ]; then
echo "Error: 実行コマンドを指定してください"
echo "Usage: op-env run [.env] -- <command>"
exit 1
fi
if [ "$1" = "--" ]; then
ENV_FILE=".env"
shift
else
ENV_FILE="${1:-.env}"
[ $# -gt 0 ] && shift
if [ "${1:-}" = "--" ]; then
shift
fi
fi
if [ $# -eq 0 ]; then
echo "Error: 実行コマンドを指定してください"
echo "Usage: op-env run [.env] -- <command>"
exit 1
fi
if [ ! -f "$ENV_FILE" ]; then
echo "Error: ${ENV_FILE} が見つかりません"
exit 1
fi
if ! grep -q 'op://' "$ENV_FILE"; then
echo "Warning: ${ENV_FILE} にop://参照が含まれていません"
echo "先に 'op-env migrate' で移行してください"
exit 1
fi
op run --env-file="$ENV_FILE" -- "$@"
sync
.envファイルと1Passwordの差分を検出・同期する。
引数: .envファイルパス、--push(.env→1Password)/ --pull(1Password→.env)
case "${1:-}" in
--diff|--push|--pull) ENV_FILE=".env"; MODE="$1" ;;
"") ENV_FILE=".env"; MODE="--diff" ;;
*) ENV_FILE="$1"; MODE="${2:---diff}" ;;
esac
if [ ! -f "$ENV_FILE" ]; then
echo "Error: ${ENV_FILE} が見つかりません"
exit 1
fi
ENV_KEYS=()
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue
[[ "$line" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]] || continue
KEY="${line%%=*}"
[ -z "$KEY" ] && continue
ENV_KEYS+=("$KEY")
done < "$ENV_FILE"
OP_JSON=$(op item get "$ITEM_NAME" --vault="$VAULT" --format=json 2>/dev/null)
if [ -z "$OP_JSON" ]; then
echo "Error: 1Passwordアイテム '${ITEM_NAME}' (vault: ${VAULT}) が見つかりません"
echo "先に 'op-env migrate' でアイテムを作成してください"
exit 1
fi
OP_FIELDS=$(echo "$OP_JSON" | op item get "$ITEM_NAME" --vault="$VAULT" --fields label --format=json 2>/dev/null | \
python3 -c "
import sys, json
try:
fields = json.load(sys.stdin)
if isinstance(fields, list):
for f in fields:
label = f.get('label', '')
if label:
print(label)
elif isinstance(fields, dict):
label = fields.get('label', '')
if label:
print(label)
except:
sys.exit(1)
" 2>/dev/null) || {
echo "Error: フィールド一覧の取得に失敗しました"
exit 1
}
case "$MODE" in
--diff)
echo "=== 差分レポート ==="
echo "Vault: ${VAULT} / Item: ${ITEM_NAME}"
echo ""
echo "--- .envのみ(1Passwordに未登録)---"
for KEY in "${ENV_KEYS[@]}"; do
if ! echo "$OP_FIELDS" | grep -qxF "$KEY"; then
echo " + $KEY"
fi
done
echo "--- 1Passwordのみ(.envに未記載)---"
while IFS= read -r FIELD; do
[ -z "$FIELD" ] && continue
FOUND=false
for KEY in "${ENV_KEYS[@]}"; do
if [ "$KEY" = "$FIELD" ]; then
FOUND=true
break
fi
done
if [ "$FOUND" = false ]; then
echo " - $FIELD"
fi
done <<< "$OP_FIELDS"
echo ""
echo "同期するには --push または --pull を指定してください"
;;
--push)
echo "=== Push: .env → 1Password ==="
PUSH_KEYS=()
for KEY in "${ENV_KEYS[@]}"; do
if ! echo "$OP_FIELDS" | grep -qxF "$KEY"; then
PUSH_KEYS+=("$KEY")
fi
done
if [ ${#PUSH_KEYS[@]} -eq 0 ]; then
echo "同期対象のキーがありません"
exit 0
fi
echo "追加対象: ${PUSH_KEYS[*]}"
echo "ユーザーに確認してください。確認が取れるまで以降の処理を実行しないこと。"
exit 0
```
確認後、以下を実行:
```bash
for KEY in "${PUSH_KEYS[@]}"; do
VALUE=$(awk -F= -v k="$KEY" '$1==k { sub(/^[^=]*=/, ""); print; exit }' "$ENV_FILE")
VALUE=$(echo "$VALUE" | sed "s/^['\"]//; s/['\"]$//")
op item edit "$ITEM_NAME" --vault="$VAULT" "${KEY}[password]=${VALUE}" \
|| { echo "Error: ${KEY} の追加に失敗"; continue; }
awk -F= -v k="$KEY" -v ref="op://${VAULT}/${ITEM_NAME}/${KEY}" \
'$1==k { print k "=" ref; next } { print }' "$ENV_FILE" > "${ENV_FILE}.tmp" \
&& mv "${ENV_FILE}.tmp" "$ENV_FILE"
echo "追加: ${KEY}"
done
;;
--pull)
echo "=== Pull: 1Password → .env ==="
while IFS= read -r FIELD; do
[ -z "$FIELD" ] && continue
FOUND=false
for KEY in "${ENV_KEYS[@]}"; do
if [ "$KEY" = "$FIELD" ]; then
FOUND=true
break
fi
done
if [ "$FOUND" = false ]; then
echo "${FIELD}=op://${VAULT}/${ITEM_NAME}/${FIELD}" >> "$ENV_FILE"
echo "追加: ${FIELD}"
fi
done <<< "$OP_FIELDS"
echo "完了: ${ENV_FILE}"
;;
*)
echo "Error: 無効なモードです: ${MODE}"
echo "Usage: op-env sync <envfile> [--diff|--push|--pull]"
exit 1
;;
esac
add
新規シークレットを1Passwordに追加し、.envにop://参照を追記する。
引数: キー名、値
KEY="$1"
VALUE="$2"
ENV_FILE="${3:-.env}"
if [ ! -f "$ENV_FILE" ]; then
echo "Error: ${ENV_FILE} が見つかりません"
exit 1
fi
if [ -z "$KEY" ] || [ -z "$VALUE" ]; then
echo "Error: キー名と値を指定してください"
echo "Usage: op-env add API_KEY sk-xxx [.env]"
echo "Warning: 値はCLI引数としてプロセスリストに表示されます"
exit 1
fi
if ! echo "$KEY" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*$'; then
echo "Error: 無効なキー名です: ${KEY}"
exit 1
fi
if op item get "$ITEM_NAME" --vault="$VAULT" >/dev/null 2>&1; then
op item edit "$ITEM_NAME" --vault="$VAULT" "${KEY}[password]=${VALUE}" \
|| { echo "Error: 1Passwordへの追加に失敗しました"; exit 1; }
else
op item create --category=login --title="$ITEM_NAME" --vault="$VAULT" "${KEY}[password]=${VALUE}" \
|| { echo "Error: 1Passwordアイテムの作成に失敗しました"; exit 1; }
fi
if grep -qE "^${KEY}=" "$ENV_FILE" 2>/dev/null; then
echo "Warning: ${KEY} は既に ${ENV_FILE} に存在します。手動で更新してください。"
else
echo "${KEY}=op://${VAULT}/${ITEM_NAME}/${KEY}" >> "$ENV_FILE"
fi
echo "追加完了: ${KEY} → op://${VAULT}/${ITEM_NAME}/${KEY}"
環境変数によるカスタマイズ
| 変数 | デフォルト | 説明 |
|---|
OP_ENV_VAULT | Development | 1Password vault名 |
OP_ENV_ITEM | プロジェクト名 | 1Passwordアイテム名 |
エラーハンドリング
- op CLI未インストール: インストールURLを表示
- 未認証:
eval $(op signin) を案内
- アイテム未存在: 自動作成 or 作成を案内
- .envファイル不在: エラーメッセージを表示
- 値の表示制御: シークレット値はstdout/stderrに含めない(詳細は「セキュリティに関する重要な注意事項」を参照)