Skip to content
Back to skills

Ops Secret Sync

ASecurity

Detects and syncs Doppler→GitHub secrets drift. Compares last-updated timestamps between Doppler and GH repo secrets; flags stale GH secrets (>24h behind Doppler); confirms with user before writing any changes. Safe to run in CI or locally.

  • 538 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 27, 2026
data-aishellbashgitapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add Lifecycle-Innovations-Limited/claude-ops --skill ops-secret-sync --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ops Secret Sync?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ops Secret Sync
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/lifecycle-innovations-limited-ops-secret-sync/badge)](https://www.skillsdirectory.com/skills/lifecycle-innovations-limited-ops-secret-sync)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ops-secret-sync
description: Detects and syncs Doppler→GitHub secrets drift. Compares last-updated timestamps between Doppler and GH repo secrets; flags stale GH secrets (>24h behind Doppler); confirms with user before writing any changes. Safe to run in CI or locally.
argument-hint: "[--repo <owner/repo>] [--project <doppler-proj>] [--config <doppler-env>] [--dry-run]"
allowed-tools:
  - Bash
  - AskUserQuestion
effort: medium
maxTurns: 20
---

# OPS ► SECRET-SYNC

Detect GitHub secrets that are stale relative to Doppler. Confirm before syncing.

## CLI/API Reference

| Command | Purpose |
|---------|---------|
| `gh secret list --repo <owner/repo> --json name,updatedAt` | List GH repo secrets with timestamps |
| `doppler secrets --project <proj> --config <env> --json` | List Doppler secrets with metadata |
| `doppler secrets get <NAME> --project <proj> --config <env> --plain` | Fetch raw value for sync |
| `gh secret set <NAME> --repo <owner/repo>` | Write secret to GH (reads stdin) |

---

## Phase 1 — Resolve arguments

Parse `$ARGUMENTS`:

- `--repo <owner/repo>` → target GitHub repo (required unless registry provides default)
- `--project <proj>` → Doppler project name (required)
- `--config <env>` → Doppler config/environment, e.g. `prd`, `stg` (default: `prd`)
- `--dry-run` → report drift only, never write

If `--repo` is missing, load `${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/registry.json` and let the user pick via `AskUserQuestion` (max 4 at a time).

If `--project` is missing, run:

```bash
doppler projects --json 2>/dev/null | jq -r '.[].slug'
```

and let the user pick via `AskUserQuestion` (max 4 at a time).

---

## Phase 2 — Fetch secret inventories

Run in parallel (background both, then collect):

```bash
# GH secrets (names + last-updated timestamps, ISO-8601)
gh secret list --repo <owner/repo> --json name,updatedAt 2>/dev/null
```

```bash
# Doppler secrets (names + metadata including updated_at)
doppler secrets --project <proj> --config <env> --json 2>/dev/null
```

Parse outputs:

**GH format** (array):
```json
[{"name": "INNGEST_SIGNING_KEY", "updatedAt": "2026-04-23T09:12:00Z"}, ...]
```

**Doppler format** (object keyed by name):
```json
{
  "INNGEST_SIGNING_KEY": {"computed": "...", "note": "", "rawValue": "...", "updatedAt": "2026-04-23T10:00:00Z"},
  ...
}
```

> Note: Doppler's `--json` flag returns computed values inline. Use `doppler secrets get <NAME> --plain` only at sync time to avoid holding all values in memory.

---

## Phase 3 — Drift detection

For each secret in Doppler:

1. Check if a GH secret with the same name exists.
2. If yes: compute `delta = doppler_updated_at - gh_updated_at` (seconds).
3. If `delta > 86400` (24 hours): mark as **DRIFTED**.
4. If GH secret does NOT exist: mark as **GH_MISSING** (flag but do not auto-create — requires explicit user confirm).

Build drift report:

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 OPS ► SECRET-SYNC — <repo> / <proj>/<config>
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 Doppler secrets : <N>
 GH repo secrets : <M>
 Matched         : <K>
 DRIFTED (>24h)  : <D>
 GH missing      : <G>

 DRIFTED SECRETS
   <NAME>  Doppler: <date>  GH: <date>  delta: <Nd>
   ...

 GH MISSING (in Doppler but absent from GH)
   <NAME>
   ...

 IN SYNC (no action needed)
   <NAME>  last-synced: <date>
   ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

If `--dry-run` is set or no drift found: stop here and display report.

---

## Phase 4 — Confirm before syncing (REQUIRED — never skip)

> Rule 5 compliance: every secret write requires per-action confirmation.

If drift was found, ask:

```
AskUserQuestion({
  title: "Sync stale GH secrets from Doppler?",
  question: "Found <D> drifted + <G> missing secrets for <repo>.\n\nSync all, pick specific ones, or skip?",
  options: [
    { value: "all",    label: "Sync all drifted + missing" },
    { value: "pick",   label: "Pick which secrets to sync" },
    { value: "drifted", label: "Sync drifted only (skip missing)" },
    { value: "skip",   label: "Skip — report only" }
  ]
})
```

**On "Pick"**: present drifted secrets 4-at-a-time via `AskUserQuestion` checkboxes. Collect user selections. Proceed only with confirmed names.

**On "Skip"**: stop. Print report path.

---

## Phase 5 — Sync confirmed secrets

For each confirmed secret name:

```bash
doppler secrets get <NAME> --project <proj> --config <env> --plain 2>/dev/null \
  | gh secret set <NAME> --repo <owner/repo>
```

After each write, verify:

```bash
gh secret list --repo <owner/repo> --json name,updatedAt \
  | jq -r '.[] | select(.name == "<NAME>") | .updatedAt'
```

Confirm the `updatedAt` advanced. If it did not advance: report failure for that secret and continue with the rest.

Print per-secret outcome:

```
  synced : INNGEST_SIGNING_KEY  (Doppler 2026-04-23 → GH updated)
  FAILED : SOME_KEY             (gh secret set exited non-zero)
```

---

## Phase 6 — Summary

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 SYNC COMPLETE — <repo>
 Synced  : <S> secrets
 Failed  : <F> secrets
 Skipped : <K> secrets
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

If failures > 0: suggest `doppler run --project <proj> --config <env> -- gh secret set <NAME> --repo <repo>` as a manual fallback.

---

## Example: recurring drift pattern

This skill exists because of a common drift pattern:

| Project | Secret | Situation |
|---------|--------|-----------|
| `<your-api>` | `INNGEST_SIGNING_KEY` | Rotated in Doppler — GH secret was 24 days stale, CI failures started after the grace period |
| `<your-service>` | `CEREBRAS_API_KEY` | Added to Doppler, never propagated to GH secrets — CI gate failed |
| `<your-service>` | `OPENROUTER_API_KEY` | Same pattern — GH missing, Doppler current |

Running `/ops:secret-sync --repo <your-org>/<your-api> --project <your-api> --config prd` would have surfaced `INNGEST_SIGNING_KEY` as DRIFTED before CI failed.

---

## Safety rules

- **Never** read or print raw secret values in the summary or logs.
- **Never** sync without user confirmation (Rule 5).
- Doppler values flow directly from `doppler secrets get --plain` into `gh secret set` via a pipe — they are never stored in shell variables, temp files, or command substitution.
- `--dry-run` always safe: no writes, report only.

---

## Mobile / SSH (Rule 7)

When `$SSH_CONNECTION` / `$OPS_MOBILE=1` / `$COLUMNS < 80`: skip the banner, emit compact lines:

```
repo: your-org/stagery-api  project: stagery-api/prd
drifted: 2  missing: 1
INNGEST_SIGNING_KEY  doppler: 2026-04-23  gh: 2026-03-30  (24d stale)
CEREBRAS_API_KEY     gh: MISSING
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.