Claude Skill

build-time-secret-injection

Use when wiring a value that ships in the binary but must stay out of public-repo diffs until launch (AdMob `GADApplicationIdentifier` / banner unit ID, a third-party SDK app key) into an Apple build via xcconfig, Info.plist `$()` substitution and a guarded `Bundle.main` read, in

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download wei18-apple-dev-skills-apple-dev-skills_skills_build-time-secret-injection-7ea7e61.zip · 6 KB
Part of wei18/apple-dev-skills — 37 skills

Install

skills CLI npx skills add https://github.com/wei18/apple-dev-skills/tree/main/apple-dev-skills/skills/build-time-secret-injection
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wei18-apple-dev-skills@llmmart
Git git clone https://github.com/wei18/apple-dev-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole wei18/apple-dev-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Build-time Secret Injection (Apple-platform)

Tuist assumption: the Project.swift snippets below assume the app's .xcodeproj is generated by Tuist from a root-level Project.swift (Tuist's own convention keeps Tuist/ for Package.swift and shared helpers, not for Project.swift itself). A hand-maintained .xcodeproj needs no Project.swift step — see Non-Tuist projects below for the equivalent (an xcconfig referenced directly from the target's Build Settings → Configurations, instead of via Project.swift).

When to invoke

Any task that introduces or wires values which are:

  • Technically app-public once the app ships (embedded in Info.plist, visible in shipped binary, observable in network traffic), AND
  • Pre-launch sensitive (committed to public repo before ship = ad-fraud reconnaissance window, convention violation among collaborators, or fingerprinting of unreleased product)

Examples:

  • AdMob App ID + Banner / Interstitial / Rewarded Unit IDs
  • ASC API .p8 key, key-id, issuer ID, ASC numeric app-id
  • Any third-party SDK app key (Firebase, RevenueCat, etc.) where the convention is "hold until ship"

Do NOT invoke for:

  • True per-deploy secrets (signing certs, CloudKit production API keys, push notification keys) — those have stricter patterns (see apple-public-repo-security)
  • Values genuinely public from day 1 (bundle IDs, CKContainer IDs, IAP product IDs, marketing URLs)

The pattern

Value's consumer Layer Storage
Xcode build / Info.plist / Bundle.main read Layer 1 — xcconfig Tuist/<Domain>.xcconfig (gitignored)
CLI tooling (swift run <CLI>, shell scripts) Layer 2 — .env secrets/.env (gitignored)
Signing certs, CloudKit server-to-server key, APNs key Not this skill → apple-public-repo-security

Two storage layers, one mechanism per layer

Layer 1 — Build-time secrets (consumed by Xcode build process)

Tuist/
  ├── <Domain>.xcconfig             # gitignored, real values
  ├── <Domain>.xcconfig.example     # committed, sandbox values + structure
  ├── Signing.xcconfig              # existing precedent (gitignored)
  └── Signing.xcconfig.example      # existing precedent (committed)
  • xcconfig holds KEY = VALUE pairs
  • Project.swift declares per-target settings(configurations: [.debug(name:, xcconfig:), .release(name:, xcconfig:)]) pointing at the file
  • Info.plist uses $(KEY) substitution to embed values at compile time — e.g. ADMOB_APP_ID = ca-app-pub-<publisher-id>~<app-id> in the xcconfig and <key>GADApplicationIdentifier</key><string>$(ADMOB_APP_ID)</string> in Info.plist (GADApplicationIdentifier is the key the Google Mobile Ads SDK reads at startup; ADMOB_APP_ID / ADMOB_BANNER_UNIT_ID are this project's own xcconfig names)
  • App code reads via Bundle.main.object(forInfoDictionaryKey: "...") — guarded against nil / empty / unresolved $() token
  • CI side (ci_scripts/ci_post_clone.sh): reads XCC env vars (stored as Secrets in ASC → Xcode Cloud → Workflow → Environment Variables) and generates the xcconfig file before tuist generate runs

Layer 2 — CLI tooling secrets (consumed by swift run <CLI> etc.)

secrets/
  ├── .env                          # gitignored, real values
  ├── .env.example                  # committed, structure + docstring
  ├── <Domain>AuthKey_*.p8          # gitignored binary cert
  └── .gitignore                    # deny-by-default: */!*.example/!README.md/!example/ /!example/**
  • .env is KEY=VALUE shell-style
  • Dev pattern: source secrets/.env && swift run <CLI> --flag-using-$KEY ...
  • CLI itself does NOT need code changes to read env automatically

Project.swift wiring (Tuist)

let appTarget = Target.target(
    // ...
    settings: .settings(
        base: ["SWIFT_VERSION": "6"],
        configurations: [
            .debug(name: "Debug", xcconfig: "Tuist/Config-Debug.xcconfig"),
            .release(name: "Release", xcconfig: "Tuist/Config-Release.xcconfig"),
        ]
    )
)

Wrap multiple xcconfigs via a Config-{Debug,Release}.xcconfig that #include? both Signing + AdMob (Tuist's xcconfig: arg takes a single path).

Multi-app dispatch in ci_post_clone.sh

When one repo ships multiple app schemes (e.g. AppA + AppB), XCC sets $CI_PRODUCT and $CI_XCODE_SCHEME per workflow. For the case-switch that picks the right env-var prefix per scheme, read references/multi-app-ci-dispatch.md.

Non-Tuist projects

If the project uses a hand-edited .xcodeproj, the equivalent storage is Config/*.xcconfig referenced via target → Build Settings → Base Configuration. Pattern is otherwise unchanged. Tuist regen / clobbering concerns don't apply; manual sync remains your responsibility.

Smoke test scope (CRITICAL)

The substitution-resolution check must run against the built bundle's Info.plist, not the source-tree Info.plist:

// ❌ WRONG — reads source plist, gets literal "$(ADMOB_BANNER_UNIT_ID)" — passes falsely
// (ADMOB_BANNER_UNIT_ID is this project's own xcconfig key name, not one Google defines —
// see the multi-app xcconfig rendering above.)
let plist = try PropertyListSerialization.propertyList(from: sourceData, ...)
#expect((plist["ADMOB_BANNER_UNIT_ID"] as? String)?.isEmpty == false)  // passes for "$(...)" string

// ✅ RIGHT — combine source-plist key-presence test + runtime guard in code
// Source test catches "someone deleted the key"; runtime guard catches "substitution failed"
guard
    let bannerID = Bundle.main.object(forInfoDictionaryKey: "ADMOB_BANNER_UNIT_ID") as? String,
    !bannerID.isEmpty,
    !bannerID.hasPrefix("$(")
else { preconditionFailure("...") }

Consider adding a build-phase script that asserts no $() literals survived substitution into the built .app/Info.plist; until one exists, the runtime guard above is the only catch.

Anti-patterns to refuse

  1. Production IDs in code comments, docstrings, PR descriptions, commit messages, or Info.plist <!-- --> blocks. Even when the value field uses a sandbox stand-in, the surrounding prose leaks production via git history. Including the literal ID anywhere in tracked text — even prefixed by TODO / FIXME / "will-replace" — IS the leak. Reference the out-of-repo vault entry or the gitignored secrets file by name; never paste the value inline.

  2. Hardcoded production IDs in Live.swift with intent to "swap before release" without an enforcement mechanism. The interim fatalError("REPLACE_BEFORE_RELEASE: ...") pattern is acceptable as a TRANSITIONAL guard paired with xcconfig migration, but is forbidden as a long-term standalone solution. Once xcconfig is in place, replace with: Info.plist $() + runtime guard verifying Bundle.main.object(forInfoDictionaryKey:) returns non-empty AND non-$(...).

  3. Conflating GitHub Secrets with XCC env vars. Apple's XCC does not read GH Secrets — they're separate storage. If CI builds on XCC, secrets must live in XCC's Environment Variables UI, not GH.

  4. Most common mistake: ❗ Shell env vars do NOT feed xcconfig $(VAR) interpolation. xcconfig variable resolution reads from the build settings table, not process env. source admob.env && xcodebuild archive does NOT populate $(ADMOB_APP_ID). Only positional xcodebuild VAR=value or -xcconfig override.xcconfig actually injects, OR a CI script writes the xcconfig file before build.

  5. Bundle.main.object(forInfoDictionaryKey:) as! String — force cast bypasses SwiftLint AND crashes hard if CI generation skipped + xcconfig missing. Use as? String + guard let ... else { preconditionFailure } with the unresolved-$() check.

  6. Bundle.main from inside a SwiftPM package is fine for app-target composition root reads but flaky for #Preview / test host / unit-test contexts. Wrap reads in a smoke test that asserts the key exists in source plist; runtime guard compensates for missing-substitution case.

  7. secrets/ or Tuist/<Domain>.xcconfig committed by accident. Use an inner secrets/.gitignore deny-list (* / !*.example / !README.md / !example/ / !example/** — the last two are required, otherwise * ignores the example/ directory and git never descends into it) PLUS root .gitignore rules Tuist/*.xcconfig + !Tuist/*.xcconfig.example so neither slips through default-add operations.

  8. Tuist tuist generate silently clobbering unmanaged xcconfigs. If Tuist/<Domain>.xcconfig exists but is NOT referenced in Project.swift's .settings(configurations:), Tuist regen drops it from the project. Verify Project.swift wiring before assuming xcconfig is active.

Verification checklist

Use this both when adding a new secret value and when auditing an existing implementation.

  • Decide the layer: Xcode build / Info.plist / Bundle.main read → Layer 1 xcconfig; swift run / CLI scripts / shell → Layer 2 secrets/.env
  • Root .gitignore has Tuist/*.xcconfig + !Tuist/*.xcconfig.example; secrets/.gitignore inner deny-list present (* / !*.example / !README.md / !example/ / !example/**) — git check-ignore -v secrets/example/README.md reports nothing
  • KEY is added to the appropriate .example file with a sandbox/test default value, with an inline comment naming the out-of-repo vault entry that holds the real value (password manager / team vault) — never the literal value
  • Project.swift per-target .settings(configurations:) references the xcconfig
  • Layer 1: Info.plist uses $(KEY) substitution for each secret; app code reads via Bundle.main.object(forInfoDictionaryKey:) with a guard (NOT as!) that rejects nil, empty, and the $(...) literal
  • Smoke test reads the source plist for a key-presence assertion
  • ci_post_clone.sh writes the xcconfig from the XCC env var (${VAR:?missing message}) BEFORE tuist generate; if multi-app, case on $CI_XCODE_SCHEME selects per-app env vars — see references/multi-app-ci-dispatch.md
  • XCC Workflow Environment Variables UI lists each KEY (per scheme if multi-app), marked Secret
  • grep -r "<real-prod-value>" . (excluding gitignored dirs) returns zero hits across all tracked files
  • The real value is recorded in the out-of-repo secret store, noting which entry holds it — never in a tracked file

Related skills

  • REQUIRED background: apple-public-repo-security — broader secret-leak prevention (gitleaks, lefthook, GitHub Secret Scanning)
  • SIBLING: monetization-sdk-integration — invoke together when wiring AdMob; this skill is the secret-handling layer
  • SIBLING: asc-api-automation — ASC API key handling (the .p8) once the key leaves the build and drives the REST API
  • Official sources: when verifying or updating a factual or version-sensitive claim, read references/official-docs.md.
Files (apple-dev-skills)
  • references
    • multi-app-ci-dispatch.md 913 B
      # Multi-App CI Dispatch
      
      ### Multi-app dispatch in `ci_post_clone.sh`
      
      When one repo ships multiple app schemes (e.g. AppA + AppB), XCC sets `$CI_PRODUCT` and `$CI_XCODE_SCHEME` per workflow. Case-switch on the scheme to pick the right env-var prefix:
      
      ```bash
      case "${CI_XCODE_SCHEME:-${CI_PRODUCT:-}}" in
        AppA)
          APP_ID="${APP_A_ADMOB_APP_ID:?missing APP_A_ADMOB_APP_ID}"
          BANNER_UNIT_ID="${APP_A_ADMOB_BANNER_UNIT_ID:?missing APP_A_ADMOB_BANNER_UNIT_ID}"
          ;;
        AppB)
          APP_ID="${APP_B_ADMOB_APP_ID:?missing APP_B_ADMOB_APP_ID}"
          BANNER_UNIT_ID="${APP_B_ADMOB_BANNER_UNIT_ID:?missing APP_B_ADMOB_BANNER_UNIT_ID}"
          ;;
        *)
          echo "Unknown CI_XCODE_SCHEME: ${CI_XCODE_SCHEME:-}" >&2
          exit 1
          ;;
      esac
      cat > Tuist/AdMob.xcconfig <<EOF
      ADMOB_APP_ID = ${APP_ID}
      ADMOB_BANNER_UNIT_ID = ${BANNER_UNIT_ID}
      EOF
      ```
      
      Run **before** `tuist generate` so the per-target xcconfig reference resolves.
      
    • official-docs.md 989 B
      Official pages backing this skill's claims; read when verifying or updating a factual or version-sensitive claim.
      
      | Page | URL | Backs |
      |---|---|---|
      | Adding a build configuration file to your project | https://developer.apple.com/documentation/xcode/adding-a-build-configuration-file-to-your-project | `#include?`, `$(VAR)` substitution |
      | Explore advanced project configuration in Xcode (WWDC21) | https://developer.apple.com/videos/play/wwdc2021/10210/ | Optional includes; build-setting precedence layers |
      | Writing custom build scripts | https://developer.apple.com/documentation/xcode/writing-custom-build-scripts | `ci_post_clone.sh` |
      | Environment variable reference | https://developer.apple.com/documentation/xcode/environment-variable-reference | `CI_XCODE_SCHEME` / `CI_PRODUCT`; custom variables are readable from custom build scripts and test actions |
      | Set up Google Mobile Ads SDK | https://developers.google.com/admob/ios/quick-start | `GADApplicationIdentifier` |
      
  • SKILL.md 11.4 KB
    ---
    name: build-time-secret-injection
    description: Use when wiring a value that ships in the binary but must stay out of public-repo diffs until launch (AdMob `GADApplicationIdentifier` / banner unit ID, a third-party SDK app key) into an Apple build via xcconfig, Info.plist `$()` substitution and a guarded `Bundle.main` read, including `ci_post_clone.sh` generation on Xcode Cloud; or when CLI tooling reads an ASC `.p8` / key ID from `secrets/.env`. Not for signing certs, CloudKit or APNs keys, nor leak prevention (gitleaks, lefthook, Secret Scanning) — see apple-public-repo-security. SDK isolation is monetization-sdk-integration.
    ---
    
    # Build-time Secret Injection (Apple-platform)
    
    **Tuist assumption**: the `Project.swift` snippets below assume the app's `.xcodeproj` is generated by Tuist from a root-level `Project.swift` (Tuist's own convention keeps `Tuist/` for `Package.swift` and shared helpers, not for `Project.swift` itself). A hand-maintained `.xcodeproj` needs no `Project.swift` step — see **Non-Tuist projects** below for the equivalent (an xcconfig referenced directly from the target's Build Settings → Configurations, instead of via `Project.swift`).
    
    ## When to invoke
    
    Any task that introduces or wires values which are:
    - Technically **app-public** once the app ships (embedded in `Info.plist`, visible in shipped binary, observable in network traffic), AND
    - **Pre-launch sensitive** (committed to public repo before ship = ad-fraud reconnaissance window, convention violation among collaborators, or fingerprinting of unreleased product)
    
    Examples:
    - AdMob App ID + Banner / Interstitial / Rewarded Unit IDs
    - ASC API `.p8` key, key-id, issuer ID, ASC numeric app-id
    - Any third-party SDK app key (Firebase, RevenueCat, etc.) where the convention is "hold until ship"
    
    Do NOT invoke for:
    - True per-deploy secrets (signing certs, CloudKit production API keys, push notification keys) — those have stricter patterns (see `apple-public-repo-security`)
    - Values genuinely public from day 1 (bundle IDs, CKContainer IDs, IAP product IDs, marketing URLs)
    
    ## The pattern
    
    | Value's consumer | Layer | Storage |
    |---|---|---|
    | Xcode build / Info.plist / `Bundle.main` read | Layer 1 — xcconfig | `Tuist/<Domain>.xcconfig` (gitignored) |
    | CLI tooling (`swift run <CLI>`, shell scripts) | Layer 2 — `.env` | `secrets/.env` (gitignored) |
    | Signing certs, CloudKit server-to-server key, APNs key | Not this skill | → `apple-public-repo-security` |
    
    ### Two storage layers, one mechanism per layer
    
    **Layer 1 — Build-time secrets (consumed by Xcode build process)**
    
    ```
    Tuist/
      ├── <Domain>.xcconfig             # gitignored, real values
      ├── <Domain>.xcconfig.example     # committed, sandbox values + structure
      ├── Signing.xcconfig              # existing precedent (gitignored)
      └── Signing.xcconfig.example      # existing precedent (committed)
    ```
    
    - xcconfig holds `KEY = VALUE` pairs
    - `Project.swift` declares per-target `settings(configurations: [.debug(name:, xcconfig:), .release(name:, xcconfig:)])` pointing at the file
    - Info.plist uses `$(KEY)` substitution to embed values at compile time — e.g. `ADMOB_APP_ID = ca-app-pub-<publisher-id>~<app-id>` in the xcconfig and `<key>GADApplicationIdentifier</key><string>$(ADMOB_APP_ID)</string>` in Info.plist (`GADApplicationIdentifier` is the key the Google Mobile Ads SDK reads at startup; `ADMOB_APP_ID` / `ADMOB_BANNER_UNIT_ID` are this project's own xcconfig names)
    - App code reads via `Bundle.main.object(forInfoDictionaryKey: "...")` — guarded against nil / empty / unresolved `$()` token
    - **CI side** (`ci_scripts/ci_post_clone.sh`): reads XCC env vars (stored as Secrets in ASC → Xcode Cloud → Workflow → Environment Variables) and generates the xcconfig file before `tuist generate` runs
    
    **Layer 2 — CLI tooling secrets (consumed by `swift run <CLI>` etc.)**
    
    ```
    secrets/
      ├── .env                          # gitignored, real values
      ├── .env.example                  # committed, structure + docstring
      ├── <Domain>AuthKey_*.p8          # gitignored binary cert
      └── .gitignore                    # deny-by-default: */!*.example/!README.md/!example/ /!example/**
    ```
    
    - `.env` is `KEY=VALUE` shell-style
    - Dev pattern: `source secrets/.env && swift run <CLI> --flag-using-$KEY ...`
    - CLI itself does NOT need code changes to read env automatically
    
    ### Project.swift wiring (Tuist)
    
    ```swift
    let appTarget = Target.target(
        // ...
        settings: .settings(
            base: ["SWIFT_VERSION": "6"],
            configurations: [
                .debug(name: "Debug", xcconfig: "Tuist/Config-Debug.xcconfig"),
                .release(name: "Release", xcconfig: "Tuist/Config-Release.xcconfig"),
            ]
        )
    )
    ```
    
    Wrap multiple xcconfigs via a `Config-{Debug,Release}.xcconfig` that `#include?` both Signing + AdMob (Tuist's `xcconfig:` arg takes a single path).
    
    ### Multi-app dispatch in `ci_post_clone.sh`
    
    When one repo ships multiple app schemes (e.g. AppA + AppB), XCC sets `$CI_PRODUCT` and `$CI_XCODE_SCHEME` per workflow. For the case-switch that picks the right env-var prefix per scheme, read `references/multi-app-ci-dispatch.md`.
    
    ### Non-Tuist projects
    
    If the project uses a hand-edited `.xcodeproj`, the equivalent storage is `Config/*.xcconfig` referenced via target → Build Settings → Base Configuration. Pattern is otherwise unchanged. Tuist regen / clobbering concerns don't apply; manual sync remains your responsibility.
    
    ### Smoke test scope (CRITICAL)
    
    The substitution-resolution check must run against the **built bundle's** Info.plist, not the source-tree Info.plist:
    
    ```swift
    // ❌ WRONG — reads source plist, gets literal "$(ADMOB_BANNER_UNIT_ID)" — passes falsely
    // (ADMOB_BANNER_UNIT_ID is this project's own xcconfig key name, not one Google defines —
    // see the multi-app xcconfig rendering above.)
    let plist = try PropertyListSerialization.propertyList(from: sourceData, ...)
    #expect((plist["ADMOB_BANNER_UNIT_ID"] as? String)?.isEmpty == false)  // passes for "$(...)" string
    
    // ✅ RIGHT — combine source-plist key-presence test + runtime guard in code
    // Source test catches "someone deleted the key"; runtime guard catches "substitution failed"
    guard
        let bannerID = Bundle.main.object(forInfoDictionaryKey: "ADMOB_BANNER_UNIT_ID") as? String,
        !bannerID.isEmpty,
        !bannerID.hasPrefix("$(")
    else { preconditionFailure("...") }
    ```
    
    Consider adding a build-phase script that asserts no `$()` literals survived substitution into the built `.app/Info.plist`; until one exists, the runtime guard above is the only catch.
    
    ## Anti-patterns to refuse
    
    1. **Production IDs in code comments, docstrings, PR descriptions, commit messages, or `Info.plist <!-- -->` blocks.** Even when the value field uses a sandbox stand-in, the surrounding prose leaks production via git history. **Including the literal ID anywhere in tracked text — even prefixed by TODO / FIXME / "will-replace" — IS the leak.** Reference the out-of-repo vault entry or the gitignored secrets file by name; never paste the value inline.
    
    2. **Hardcoded production IDs in `Live.swift` with intent to "swap before release"** without an enforcement mechanism. The interim `fatalError("REPLACE_BEFORE_RELEASE: ...")` pattern is acceptable as a TRANSITIONAL guard paired with xcconfig migration, but is forbidden as a long-term standalone solution. Once xcconfig is in place, replace with: Info.plist `$()` + runtime guard verifying `Bundle.main.object(forInfoDictionaryKey:)` returns non-empty AND non-`$(...)`.
    
    3. **Conflating GitHub Secrets with XCC env vars.** Apple's XCC does not read GH Secrets — they're separate storage. If CI builds on XCC, secrets must live in XCC's Environment Variables UI, not GH.
    
    4. **Most common mistake**: ❗ **Shell env vars do NOT feed xcconfig `$(VAR)` interpolation.** xcconfig variable resolution reads from the build settings table, not process env. `source admob.env && xcodebuild archive` does NOT populate `$(ADMOB_APP_ID)`. Only positional `xcodebuild VAR=value` or `-xcconfig override.xcconfig` actually injects, OR a CI script writes the xcconfig file before build.
    
    5. **`Bundle.main.object(forInfoDictionaryKey:) as! String`** — force cast bypasses SwiftLint AND crashes hard if CI generation skipped + xcconfig missing. Use `as? String` + `guard let ... else { preconditionFailure }` with the unresolved-`$()` check.
    
    6. **`Bundle.main` from inside a SwiftPM package** is fine for app-target composition root reads but flaky for #Preview / test host / unit-test contexts. Wrap reads in a smoke test that asserts the key exists in source plist; runtime guard compensates for missing-substitution case.
    
    7. **`secrets/` or `Tuist/<Domain>.xcconfig` committed by accident.** Use an inner `secrets/.gitignore` deny-list (`* / !*.example / !README.md / !example/ / !example/**` — the last two are required, otherwise `*` ignores the `example/` directory and git never descends into it) PLUS root `.gitignore` rules `Tuist/*.xcconfig` + `!Tuist/*.xcconfig.example` so neither slips through default-add operations.
    
    8. **Tuist `tuist generate` silently clobbering unmanaged xcconfigs.** If `Tuist/<Domain>.xcconfig` exists but is NOT referenced in `Project.swift`'s `.settings(configurations:)`, Tuist regen drops it from the project. Verify Project.swift wiring before assuming xcconfig is active.
    
    ## Verification checklist
    
    Use this both when adding a new secret value and when auditing an existing implementation.
    
    - [ ] Decide the layer: Xcode build / Info.plist / `Bundle.main` read → Layer 1 xcconfig; `swift run` / CLI scripts / shell → Layer 2 `secrets/.env`
    - [ ] Root `.gitignore` has `Tuist/*.xcconfig` + `!Tuist/*.xcconfig.example`; `secrets/.gitignore` inner deny-list present (`* / !*.example / !README.md / !example/ / !example/**`) — `git check-ignore -v secrets/example/README.md` reports nothing
    - [ ] KEY is added to the appropriate `.example` file with a sandbox/test default value, with an inline comment naming the out-of-repo vault entry that holds the real value (password manager / team vault) — never the literal value
    - [ ] `Project.swift` per-target `.settings(configurations:)` references the xcconfig
    - [ ] Layer 1: `Info.plist` uses `$(KEY)` substitution for each secret; app code reads via `Bundle.main.object(forInfoDictionaryKey:)` with a guard (NOT `as!`) that rejects `nil`, empty, and the `$(...)` literal
    - [ ] Smoke test reads the source plist for a key-presence assertion
    - [ ] `ci_post_clone.sh` writes the xcconfig from the XCC env var (`${VAR:?missing message}`) BEFORE `tuist generate`; if multi-app, `case` on `$CI_XCODE_SCHEME` selects per-app env vars — see `references/multi-app-ci-dispatch.md`
    - [ ] XCC Workflow Environment Variables UI lists each KEY (per scheme if multi-app), marked Secret
    - [ ] `grep -r "<real-prod-value>" .` (excluding gitignored dirs) returns zero hits across all tracked files
    - [ ] The real value is recorded in the out-of-repo secret store, noting which entry holds it — never in a tracked file
    
    ## Related skills
    
    - **REQUIRED background**: `apple-public-repo-security` — broader secret-leak prevention (gitleaks, lefthook, GitHub Secret Scanning)
    - **SIBLING**: `monetization-sdk-integration` — invoke together when wiring AdMob; this skill is the secret-handling layer
    - **SIBLING**: `asc-api-automation` — ASC API key handling (the `.p8`) once the key leaves the build and drives the REST API
    - Official sources: when verifying or updating a factual or version-sensitive claim, read `references/official-docs.md`.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related