Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Build Time Secret Injection

BSecurity

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 ...

18 stars
0 votes
0 copies
0 views
Added 9/19/2026
developmentgoswiftshellgitapisecurity

Works with

cliapi

Security Analysis

B75/100
criticalSends environment variables or credentials to an external URL

Scanned 9/19/2026

Install to Claude Code

$npx -y skills add wei18/apple-dev-skills --skill build-time-secret-injection --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Build Time Secret Injection?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Build Time Secret Injection
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/wei18-build-time-secret-injection/badge)](https://www.skillsdirectory.com/skills/wei18-build-time-secret-injection)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
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`.

Attribution

wei18wei18
View sourceMore from wei18 →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

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 (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

281612 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2132 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Tanstack Start

Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per ...

9881 votes

Pentest

PTES-aligned adversarial security audit for backend, frontend, and mobile applications. Produces a CVSS-scored Hacker Report with verified PoCs and phased remediation.

5491 votes
View all in development →