
Gpc Security
- 27 installs
- 1 repo stars
- Updated August 1, 2026
- yasserstudio/gpc-skills
Helps with security tasks.
About
gpc-security is a Claude Code skill for security. It helps solo builders move faster with AI-assisted coding.
- gpc-security
- Security
- AI-coding skill
Gpc Security by the numbers
- 27 all-time installs (skills.sh)
- Ranked #1,530 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yasserstudio/gpc-skills --skill gpc-securityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 27 |
|---|---|
| repo stars | ★ 1 |
| Last updated | August 1, 2026 |
| Repository | yasserstudio/gpc-skills ↗ |
What it does
Helps with security tasks.
Files
gpc-security
Credential management, audit logging, and security best practices for GPC.
When to use
- Securing service account keys and credentials
- Setting up credential rotation
- Reviewing audit logs for compliance
- Handling a compromised service account key
- Securing GPC in CI/CD pipelines
- Understanding where GPC stores sensitive data
Inputs required
- GPC installed and authenticated —
gpc auth status - Service account key files — for rotation procedures
- CI/CD platform access — for updating secrets
Procedure
0. Credential storage locations
GPC stores credentials in platform-appropriate secure locations:
| Data | Location | Security |
|---|---|---|
| OAuth tokens | OS keychain (macOS/Linux/Windows) | OS-managed encryption |
| Token cache | ~/.cache/gpc/tokens/ | File permissions (0600) |
| User config | ~/.config/gpc/config.json | File permissions |
| Project config | .gpcrc.json | Version-controlled (no secrets!) |
| Audit log | ~/.config/gpc/audit.log | JSON Lines, append-only |
XDG overrides: XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME
Read: references/credential-storage.md for detailed storage architecture and security model.
1. Service account key security
Never commit keys to git
# .gitignore
*.json.key
*-sa.json
service-account*.json
play-store-key.jsonUse environment variables in CI
# GitHub Actions — key stored as secret
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SA_KEY }}Never store keys in:
.gpcrc.json(version-controlled)- Dockerfiles or docker-compose files
- Shell scripts committed to git
- CI config files (even if they seem private)
2. Key rotation
Rotate service account keys periodically (recommended: every 90 days).
# 1. Create new key in Google Cloud Console
# IAM & Admin → Service Accounts → Keys → Add Key
# 2. Test new key locally
gpc auth login --service-account /path/to/new-key.json
gpc doctor
# 3. Update CI secrets with new key
# GitHub: Settings → Secrets → PLAY_SA_KEY → Update
# GitLab: Settings → CI/CD → Variables → PLAY_SA_KEY → Update
# 4. Verify CI works with new key
# Trigger a test pipeline
# 5. Delete old key in Google Cloud Console
# IAM & Admin → Service Accounts → Keys → Delete old key
# 6. Clear local token cache
rm -rf ~/.cache/gpc/tokens/Read: references/key-rotation.md for automated rotation patterns and multi-environment strategies.
3. Audit logging
GPC logs all commands to ~/.config/gpc/audit.log in JSON Lines format:
# View recent audit entries
tail -20 ~/.config/gpc/audit.log | jq .
# Filter by command
cat ~/.config/gpc/audit.log | jq 'select(.command == "releases upload")'
# Filter by app
cat ~/.config/gpc/audit.log | jq 'select(.app == "com.example.app")'
# Filter failures
cat ~/.config/gpc/audit.log | jq 'select(.success == false)'
# Filter by date range
cat ~/.config/gpc/audit.log | jq 'select(.timestamp >= "2025-03-01")'Audit entry structure
{
"timestamp": "2025-03-09T14:30:00.000Z",
"command": "releases upload",
"app": "com.example.app",
"args": { "track": "beta", "file": "app-release.aab" },
"user": "sa@project.iam.gserviceaccount.com",
"success": true,
"durationMs": 12340
}4. Secrets redaction
GPC automatically redacts sensitive data in all output:
- Service account JSON content is never logged
- Access tokens are never shown in verbose output
- Private keys are never included in error messages
--jsonoutput redacts credential fields- Webhook payloads are redacted via
redactSensitive()before dispatch to Slack/Discord/custom endpoints (v0.9.80+) - Auth error messages redact long inputs that look like pasted credentials (v0.9.80+)
- ADC token cache uses hash-based keys per credential source to prevent multi-account confusion (v0.9.80+)
- Project
.gpcrc.jsoncannot self-approve plugins --approvedPluginsis only trusted from user config (v0.9.80+)
5. Least-privilege permissions
Grant only the permissions each service account needs:
Upload-only service account
Play Console permissions:
- View app information
- Manage testing (for internal/alpha/beta)
- Release to production (only if needed)
Read-only monitoring service account
Play Console permissions:
- View app information
- View financial data (for reports)
# Verify what a service account can do
gpc auth status --json | jq '.email'
# Then check that email's permissions in Play Console6. Handling compromised keys
If a service account key is leaked:
# 1. IMMEDIATELY delete the compromised key in Google Cloud Console
# IAM & Admin → Service Accounts → Keys → Delete
# 2. Create a new key
# Same page → Add Key → JSON
# 3. Update all locations using the key
gpc auth login --service-account /path/to/new-key.json
# 4. Update CI secrets
# All platforms using the old key
# 5. Clear token cache
rm -rf ~/.cache/gpc/tokens/
# 6. Review audit log for unauthorized actions
cat ~/.config/gpc/audit.log | jq 'select(.timestamp >= "LEAK_DATE")'
# 7. Review Google Cloud audit logs
# Cloud Console → IAM & Admin → Audit Logs7. CI/CD security patterns
GitHub Actions
# npm publish: OIDC via Trusted Publisher (v0.9.77+, no stored NPM_TOKEN)
permissions:
id-token: write # Required for OIDC token exchange with npm
# Google Play service account: encrypted secret
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SA_KEY }}
# Restrict to specific branches
if: github.ref == 'refs/heads/main'
# Use environments for approval gates (staged publishing uses this)
environment: productionSecret scanning
# Check if keys are in git history
git log --all --full-history -p -- '*.json' | grep -l '"private_key"'
# If found, rotate immediately and clean git historyVerification
gpc auth statusshows the expected service account emailgpc doctorpasses all checks.gpcrc.jsoncontains no secrets or key paths- Audit log at
~/.config/gpc/audit.logis being written - CI secrets are encrypted and not visible in logs
- Old keys are deleted after rotation
Failure modes / debugging
| Symptom | Likely Cause | Fix |
|---|---|---|
| Key file committed to git | Not in .gitignore | Add to .gitignore; rotate key immediately |
| Token cache stale after rotation | Old cached tokens | Delete ~/.cache/gpc/tokens/ |
| Audit log not writing | Config dir not writable | Check permissions on ~/.config/gpc/ |
| Service account email unknown | Key not inspected | `gpc auth status --json \ |
| CI shows credential in logs | Key passed as argument | Use environment variables, never CLI args |
| Keychain prompt every command | macOS keychain access not granted | Click "Always Allow" on the prompt |
8. Supply chain protection (v0.9.77+)
GPC uses 15 layers of defense against dependency and publish supply chain attacks:
| Layer | What it does |
|---|---|
| Trusted Publisher (OIDC) | npm publish authenticates via GitHub OIDC -- no long-lived NPM_TOKEN stored anywhere |
| Staged Publishing | CI stages packages; maintainer approves with 2FA before they go live on npm |
| NPM_TOKEN deleted | No stored npm token in GitHub secrets -- OIDC is the only auth path |
min-release-age=7 in .npmrc | Blocks packages published less than 7 days ago |
pnpm-lock.yaml | Exact version pinning, no unexpected upgrades |
| Socket.dev CI scan | socket ci on every PR, blocks on critical alerts |
| Socket.dev GitHub App | Inline PR comments on risky dependency changes |
pnpm audit in CI | Gates PRs on high-severity CVEs (production deps) |
| GitHub Actions SHA pins | All 14 action refs pinned to commit hashes, not mutable tags |
| SBOM (CycloneDX) | Bill of materials generated and archived on every npm release |
| CODEOWNERS | Security-sensitive paths require explicit review |
| Dependabot | Weekly update PRs (direct dependencies only, actions grouped) |
| Socket CLI wrapper | Scans every local npm install and npx |
| CodeQL | Static analysis on every push |
| GitHub secret scanning | Blocks pushes containing 200+ secret patterns |
GPC only has 4 runtime dependencies: google-auth-library, commander, protobufjs, yauzl. All API calls use Node.js built-in fetch.
Configuration: socket.yml at repo root controls Socket.dev alert rules. .npmrc controls min-release-age. .github/CODEOWNERS controls review requirements. Release workflow uses pnpm release-staged with OIDC authentication.
9. Security audit posture (v0.9.80 + v0.9.82)
v0.9.80 deepsec re-scan: A full-codebase deepsec audit was run after the v0.9.80 security fixes. Result: 0 new findings. All previously tracked findings from the v0.9.74 audit were resolved.
Webhook redaction (v0.9.80): Webhook payloads dispatched to Slack, Discord, and custom endpoints via --webhook-url are now redacted before dispatch. Sensitive fields are stripped from the payload before it leaves the process. This applies to all gpc watch breach events and any other webhook dispatch paths.
google-auth-library bump (v0.9.82): google-auth-library was upgraded to 10.7.0. This clears the only remaining tracked production audit finding: a brace-expansion transitive vulnerability. GPC now has zero production audit findings.
9a. GPC GitHub Action security
The GPC GitHub Action (yasserstudio/gpc-action) is a TypeScript action running on Node 24 with the following security properties:
- OIDC auth: The action authenticates to Google Play using OIDC token exchange. No long-lived secrets are stored in the action itself.
- Built-in preflight gate: The action runs
gpc preflightbefore upload. A failing preflight scan blocks the publish step. - No stored NPM token: The action uses Trusted Publisher (OIDC) for any npm operations. No
NPM_TOKENis stored in GitHub secrets. - Node 24 runtime: Matches the current GPC CLI CI matrix for consistency.
Usage:
- uses: yasserstudio/gpc-action@v1
with:
service-account: ${{ secrets.PLAY_SA_KEY }}
package-name: com.example.app
aab: app/build/outputs/bundle/release/app-release.aab
track: beta10. Developer verification
Google's Android developer verification enforcement begins September 2026 (BR, ID, SG, TH):
gpc verify # Status, deadlines, resources
gpc verify --open # Open verification page in browser
gpc verify --json # Machine-readable outputgpc doctor includes a verification check. gpc status shows a footer reminder. gpc preflight shows a post-scan reminder.
11. Signing key verification (v0.9.75+)
Verify your local signing key matches the Play signing certificate:
gpc doctor --verify # Show Play cert fingerprint
gpc doctor --verify --keystore release.keystore --store-pass $PW # Compare local vs PlayIf fingerprints don't match, you're distributing with a different key than Play uses. Register it in Play Console to avoid installation blocks after September 30, 2026.
Verification
gpc auth statusshows the expected service account emailgpc doctorpasses all checks.gpcrc.jsoncontains no secrets or key paths- Audit log at
~/.config/gpc/audit.logis being written - CI secrets are encrypted and not visible in logs
- Old keys are deleted after rotation
Related skills
- gpc-setup — initial authentication and configuration
- gpc-user-management — managing team access and permissions
- gpc-ci-integration — secure CI/CD pipeline configuration
- gpc-troubleshooting — debugging auth errors
{
"skill_name": "gpc-security",
"evals": [
{
"id": 1,
"prompt": "I think our service account key may have been accidentally committed to a public repo branch before we noticed and removed it. The key is for our main Play Console app. What should I do right now?",
"expected_output": "Provides immediate incident response steps for a compromised key",
"files": [],
"expectations": [
"First step: immediately delete the compromised key in Google Cloud Console",
"Create a new key and re-authenticate with gpc auth login",
"Update all CI secrets with the new key",
"Review audit logs for unauthorized actions during the exposure window",
"Mentions clearing token cache and checking git history for other leaks"
]
},
{
"id": 2,
"prompt": "Our security team wants to know where GPC stores credentials on developer machines and whether there's an audit trail of all Play Store operations. Can you give me a complete picture?",
"expected_output": "Explains credential storage locations, security model, and audit logging",
"files": [],
"expectations": [
"Lists all storage locations: OS keychain, ~/.cache/gpc/tokens/, ~/.config/gpc/",
"Explains that .gpcrc.json never contains secrets",
"Describes the audit log at ~/.config/gpc/audit.log with JSON Lines format",
"Mentions automatic secrets redaction in all output",
"Notes file permissions (0600) on sensitive files"
]
},
{
"id": 3,
"prompt": "We need to rotate our service account key. We use the same key in GitHub Actions, our staging server, and local dev. How do we rotate without breaking anything?",
"expected_output": "Provides a zero-downtime rotation procedure with overlap period",
"files": [],
"expectations": [
"Creates new key while old key is still active (overlap period)",
"Tests new key locally with gpc doctor before updating CI",
"Updates CI secrets (GitHub Actions, staging) with the new key",
"Verifies CI pipelines pass before deleting the old key",
"Clears token cache after rotation"
]
},
{
"id": 4,
"prompt": "A security audit flagged that our GPC-based GitHub Actions workflow exposes the PLAY_SA_KEY secret to all steps in the job, including third-party actions. How should we fix this?",
"expected_output": "Explains step-scoped secrets pattern from v0.9.74 CI template",
"files": [],
"expectations": [
"Explains the difference between job-level env and step-level env in GitHub Actions",
"Provides example of scoping GPC_SERVICE_ACCOUNT to the specific step that calls gpc",
"Explains that step-scoped secrets are not accessible to other steps including third-party actions",
"References the v0.9.74 CI template convention"
]
},
{
"id": 5,
"prompt": "How does GPC prevent a malicious plugin from executing arbitrary code? I'm building a plugin and want to understand the trust model.",
"expected_output": "Explains the plugin trust model: isPluginTrusted(), FIRST_PARTY_PLUGINS, approved set, no import() before trust check",
"files": [],
"expectations": [
"Explains that isPluginTrusted() is called before import()",
"Mentions FIRST_PARTY_PLUGINS set for built-in plugins",
"Explains user-approved set for third-party plugins via gpc plugins add",
"States that untrusted specifiers are skipped before any module loading",
"Notes this prevents top-level module code execution from untrusted sources"
]
},
{
"id": 6,
"prompt": "I want to run a security audit on the GPC codebase using deepsec. What's the process?",
"expected_output": "Explains the deepsec audit workflow: pnpm security:deep, CI integration, triage process",
"files": [],
"expectations": [
"Shows pnpm security:deep command or the individual npx deepsec steps",
"Explains that deepsec runs as a CI job on every push",
"Mentions that findings are exported as artifacts for review",
"Explains the triage workflow: review findings, fix, revalidate",
"Notes that the v0.9.74 release resolved 16 findings from an initial deepsec audit"
]
}
]
}
Credential Storage Architecture
How GPC stores and protects credentials across platforms.
Storage hierarchy
User-level (per OS user)
├── ~/.config/gpc/
│ ├── config.json # User config (may reference key paths)
│ └── audit.log # Command audit trail
├── ~/.cache/gpc/
│ └── tokens/ # Cached access tokens (auto-expire)
└── OS Keychain # OAuth refresh tokens (encrypted)
Project-level (per repository)
└── .gpcrc.json # Project config (NEVER contains secrets)What goes where
| Data | Storage | Persists | Encrypted |
|---|---|---|---|
| Service account key path | config.json or env var | Yes | No (path only) |
| Service account JSON content | GPC_SERVICE_ACCOUNT env var | Session | No |
| OAuth refresh token | OS keychain | Yes | Yes (OS-managed) |
| Access token (cached) | ~/.cache/gpc/tokens/ | 1 hour | No (short-lived) |
| App package name | .gpcrc.json or config | Yes | No (not sensitive) |
| Audit entries | audit.log | Yes | No |
Security boundaries
Safe to commit (.gpcrc.json)
{
"app": "com.example.app",
"output": "json",
"plugins": ["@gpc-cli/plugin-ci"]
}Never commit
- Service account JSON key files
- Access tokens
- OAuth credentials
- Private keys
File permissions
GPC sets appropriate permissions on creation:
| File | Permission | Reason |
|---|---|---|
config.json | 0600 | May contain key file paths |
tokens/*.json | 0600 | Contains access tokens |
audit.log | 0600 | Contains command history |
OS keychain integration
| OS | Backend | Tool |
|---|---|---|
| macOS | Keychain Access | security CLI |
| Linux | libsecret / GNOME Keyring | secret-tool |
| Windows | Windows Credential Manager | cmdkey |
Fallback when no keychain is available: ~/.config/gpc/credentials.json (file-based, less secure).
CI/CD credential patterns
GitHub Actions (recommended — v0.9.74 pattern)
Scope secrets to the step level, not the job level. This limits exposure to only the steps that need the credential.
# Step-scoped (correct — v0.9.74)
steps:
- name: Upload to Play Store
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SA_KEY }}
run: gpc releases upload --app com.example.app --file app.aab
# Job-scoped (avoid — exposes secret to all steps including third-party actions)
# env:
# GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SA_KEY }}Secrets are:
- Encrypted at rest (NaCl sealed box)
- Not visible in logs (auto-masked)
- Not available in PRs from forks
GitLab CI
variables:
GPC_SERVICE_ACCOUNT: $PLAY_SA_KEY # Protected variableEnvironment variable formats
GPC_SERVICE_ACCOUNT accepts: 1. File path: /path/to/key.json 2. Raw JSON: {"type": "service_account", ...}
GPC auto-detects which format is provided.
Subprocess env isolation
When GPC spawns subprocesses (e.g., gpc install-skills running npx skills add), it does not pass through process.env. Instead it builds an explicit safeEnv object containing only an allowlist of non-sensitive keys: PATH, HOME, USER, SHELL, TMPDIR, LANG, LC_ALL, NODE_ENV, NODE_PATH, NODE_OPTIONS, NODE_EXTRA_CA_CERTS, npm_config_registry, npm_config_cache, and proxy variables.
GPC_SERVICE_ACCOUNT, GOOGLE_APPLICATION_CREDENTIALS, CI tokens, and any other secrets present in the parent process environment are never forwarded to subprocesses.
Token lifecycle
Service Account Key
│
├─ resolveAuth() loads key
│
├─ getAccessToken()
│ ├─ Check cache → return if valid
│ ├─ JWT grant → Google OAuth2
│ └─ Cache new token (3600s TTL)
│
└─ Token expires → auto-refresh on next callAccess tokens are short-lived (1 hour) and auto-refreshed. Even if a cached token is leaked, it expires quickly.
Service Account Key Rotation
Procedures for rotating service account keys safely.
Manual rotation (recommended every 90 days)
Step 1: Create new key
# Google Cloud Console:
# IAM & Admin → Service Accounts → [your SA] → Keys → Add Key → JSON
# Download the new key fileStep 2: Test locally
# Authenticate with new key
gpc auth login --service-account /path/to/new-key.json
# Verify
gpc doctor
gpc releases list --app com.example.appStep 3: Update CI secrets
GitHub Actions
# Using gh CLI
gh secret set PLAY_SA_KEY < /path/to/new-key.jsonGitLab CI
# Settings → CI/CD → Variables → Update PLAY_SA_KEYStep 4: Verify CI
Trigger a test pipeline to confirm the new key works in CI.
Step 5: Delete old key
# Google Cloud Console:
# IAM & Admin → Service Accounts → [your SA] → Keys → Delete old keyStep 6: Clean up
# Clear local token cache
rm -rf ~/.cache/gpc/tokens/
# Securely delete old key file
rm -P /path/to/old-key.json # macOS secure delete
shred -u /path/to/old-key.json # Linux secure deleteMulti-environment rotation
When you have separate keys for dev/staging/production:
# Rotate one environment at a time
# 1. Create new prod key
# 2. Update prod CI secret
# 3. Verify prod pipeline
# 4. Delete old prod key
# 5. Repeat for staging
# 6. Repeat for devNever rotate all environments at once — if something goes wrong, you want at least one working key.
Overlap period
Google allows multiple active keys per service account (up to 10). Use this for zero-downtime rotation:
Day 0: Create new key (old key still active)
Day 1: Update CI secrets with new key
Day 2: Verify all pipelines pass
Day 3: Delete old keyAutomated rotation with scripts
#!/bin/bash
# rotate-key.sh — automated key rotation
SA_EMAIL="gpc-sa@project-id.iam.gserviceaccount.com"
PROJECT="project-id"
NEW_KEY_PATH="/tmp/new-key.json"
# Create new key
gcloud iam service-accounts keys create "$NEW_KEY_PATH" \
--iam-account="$SA_EMAIL" \
--project="$PROJECT"
# Test new key
GPC_SERVICE_ACCOUNT=$(cat "$NEW_KEY_PATH") gpc doctor
if [ $? -ne 0 ]; then
echo "New key failed verification!"
exit 1
fi
# Update GitHub secret
gh secret set PLAY_SA_KEY < "$NEW_KEY_PATH"
echo "Key rotated. After verifying CI, delete old keys:"
gcloud iam service-accounts keys list \
--iam-account="$SA_EMAIL" \
--project="$PROJECT"
# Clean up
rm -P "$NEW_KEY_PATH"Key hygiene checklist
- [ ] Keys are not committed to git (check with
git log --all -p -- '*.json' | grep private_key) - [ ] Keys are rotated every 90 days
- [ ] Old keys are deleted promptly
- [ ] CI secrets are updated before deleting old keys
- [ ] CI secrets are scoped to steps, not jobs (v0.9.74 pattern)
- [ ] Token cache is cleared after rotation
- [ ] Only necessary permissions are granted to service accounts
NPM token rotation
GPC's npm publish automation uses a granular NPM_TOKEN with a 90-day expiry. When rotating:
1. Generate a new granular token at npmjs.com (scoped to the @gpc-cli org, automation type). 2. Update the NPM_TOKEN secret in the GitHub repository settings. 3. Verify by triggering a dry-run release workflow. 4. Revoke the old token at npmjs.com immediately after confirming the new one works.
The same overlap-then-delete pattern used for Google service account keys applies here: create first, update CI, verify, then revoke the old token.
Runtime Security Hardening
GPC v0.9.74 resolved 16 findings from a deepsec (Vercel Labs AI security scanner) audit. This document covers the threat model, code location, and fix for each finding, plus design conventions that apply across the codebase.
Design conventions
p() helper — URL path encoding
File: packages/api/src/client.ts
const p = (segment: string): string => encodeURIComponent(segment);Every user-supplied path segment in an API URL must be wrapped with p(). This is a codebase-wide convention introduced in v0.9.74.
// Correct
`/applications/${p(packageName)}/edits/${p(editId)}/tracks/${p(track)}`
// Wrong — do not do this
`/applications/${packageName}/edits/${editId}/tracks/${track}`Without p(), a package name containing /, ?, or # could alter the URL structure and target unintended API endpoints.
redactPath() — HTTP error message sanitization
File: packages/api/src/http.ts
const SENSITIVE_PATH_SEGMENTS = /\/(tokens|purchases|purchaseToken)\/([^/?#]*)/gi;
function redactPath(path: string): string {
return path.replace(SENSITIVE_PATH_SEGMENTS, (_match, segment: string) => {
return `/${segment}/***REDACTED***`;
});
}redactPath() is called on every path before it appears in an error message, timeout message, or --json error output. This is applied globally at the HTTP client layer — individual commands do not need their own redaction logic for URL paths.
Env allowlist — subprocess spawning
File: packages/cli/src/commands/install-skills.ts
When spawning a subprocess (e.g., npx skills add), GPC builds an explicit safeEnv object rather than passing through process.env. Only these keys are forwarded:
PATH, HOME, USER, SHELL, TMPDIR, LANG, LC_ALL,
NODE_ENV, NODE_PATH, NODE_OPTIONS, NODE_EXTRA_CA_CERTS,
npm_config_registry, npm_config_cache,
HTTPS_PROXY, HTTP_PROXY, NO_PROXY,
https_proxy, http_proxy, no_proxyThis prevents the subprocess from inheriting sensitive variables (GPC_SERVICE_ACCOUNT, GOOGLE_APPLICATION_CREDENTIALS, CI tokens, etc.) that happen to be set in the parent environment.
Pattern for new subprocess calls:
const allowedEnvKeys = new Set(["PATH", "HOME", /* ... */]);
const safeEnv: Record<string, string> = {};
for (const [k, v] of Object.entries(process.env)) {
if (v !== undefined && allowedEnvKeys.has(k)) {
safeEnv[k] = v;
}
}
execFileSync("npx", args, { env: safeEnv });Finding catalog (v0.9.74)
F-01: Plugin RCE via untrusted import()
Severity: Critical File: packages/core/src/plugins.ts Threat: A plugin specifier pointing to a malicious local or remote module could execute arbitrary code at import time (top-level module code runs before any exports are inspected).
Fix: isPluginTrusted(specifier, approved) is called before every import(). Only specifiers in the FIRST_PARTY_PLUGINS set or in the user's approved set are loaded. Untrusted specifiers are skipped entirely.
function isPluginTrusted(specifier: string, approved?: Set<string>): boolean { ... }
// In the plugin loader loop:
if (!isPluginTrusted(name, approved)) continue;
const mod = await import(name);Guidance: Never call import() on a user-supplied or config-supplied value without passing it through isPluginTrusted() first.
---
F-02: SSRF via crafted resumable upload session URI
Severity: High File: packages/api/src/resumable-upload.ts Threat: The Google API returns a Location header with the resumable upload session URI. A compromised or MITM'd response could return a URI pointing to an internal service (e.g., http://169.254.169.254/...).
Fix: validateSessionUri(sessionUri, uploadUrl) is called before the session URI is used. It parses both URLs and checks that the session URI hostname either matches the original upload hostname or ends with .googleapis.com.
function validateSessionUri(sessionUri: string, uploadUrl: string): void {
const session = new URL(sessionUri);
const upload = new URL(uploadUrl);
if (session.hostname !== upload.hostname && !session.hostname.endsWith(".googleapis.com")) {
throw new PlayApiError(`Session URI host "${session.hostname}" does not match ...`);
}
}---
F-03: Symlink traversal in --notes-dir
Severity: High File: packages/core/src/utils/release-notes.ts Threat: A crafted notes directory containing symlinks to /etc/passwd, ~/.config/gpc/config.json, or other sensitive files could cause GPC to read and include those files as release notes.
Fix: lstat() is called on each directory entry before readFile(). If stats.isSymbolicLink() is true, the entry is skipped.
const stats = await lstat(filePath);
if (stats.isSymbolicLink()) continue; // reject symlinks
const content = await readFile(filePath, "utf-8");---
F-04: Config set echoes sensitive values
Severity: Medium File: packages/cli/src/commands/config.ts Threat: gpc config set auth.serviceAccount /path/to/key.json previously echoed the full value, which could appear in CI logs.
Fix: The confirmation message prints the key name only: Set ${key}. The value is never echoed.
---
F-05: Doctor exposes proxy credentials
Severity: Medium File: packages/cli/src/commands/doctor.ts Threat: If HTTPS_PROXY=http://user:password@proxy:8080, the doctor output would show the full URL including credentials.
Fix: checkProxy() strips credentials before display using new URL(): only protocol + host + pathname are shown.
const safeUrl = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;---
F-06: Skills installer env passthrough
Severity: Medium File: packages/cli/src/commands/install-skills.ts Threat: execFileSync("npx", args, { env: process.env }) would pass GPC_SERVICE_ACCOUNT, GOOGLE_APPLICATION_CREDENTIALS, and any other secrets to the npx subprocess.
Fix: Replaced with an explicit env allowlist (see "Env allowlist" design convention above).
---
F-07: Vitals gate checks threshold after rollout mutation
Severity: Medium Files: packages/core/ (vitals gate, rollout commands) Threat: The rollout percentage could increment and then fail the crash/ANR gate check, leaving the app at a higher-than-intended rollout with a degraded vitals signal.
Fix: Threshold gate evaluation runs before any rollout state is mutated. If the gate fails (exit code 6), the rollout increase is never written.
---
F-08: Image upload/delete ignores --dry-run
Severity: Medium Files: packages/core/src/commands/image-sync.ts, packages/core/src/commands/listings.ts Threat: --dry-run was only checked before the final edit commit, not before the individual upload and delete API calls. Images could be mutated even in dry-run mode.
Fix: dryRun is now checked before each upload and delete call:
if (!options?.dryRun) {
await client.images.upload(...);
}---
F-09: API paths missing encodeURIComponent
Severity: Medium Files: Multiple files under packages/api/src/ Threat: User-supplied values (package names, email addresses, track names) interpolated directly into URL paths could contain characters that alter URL structure.
Fix: All path parameters now use the p() helper (see design convention above). The fix was applied uniformly across all API client files.
---
F-10: RTDN exposes full purchase tokens
Severity: Medium File: packages/core/src/commands/rtdn.ts Threat: Decoded RTDN notification output included full purchaseToken values, which are sensitive identifiers that should not appear in logs or CLI output.
Fix: Purchase tokens are truncated to the first 16 characters: n.purchaseToken.slice(0, 16) + "...". The full token is never surfaced.
---
F-11: HTTP errors expose sensitive URL segments
Severity: Medium File: packages/api/src/http.ts Threat: Error messages, timeout messages, and JSON error output included raw API paths, which could contain purchase tokens, user email fragments, or other sensitive data.
Fix: All paths pass through redactPath() before appearing in any error output (see design convention above). Applied at the HTTP client layer so every command inherits the protection.
---
F-12: Webhook payload includes sensitive CLI flags
Severity: Medium File: packages/core/src/utils/webhooks.ts Threat: The argv field in webhook notification payloads could include flags like --service-account /path/to/key.json if those flags were present in the process arguments.
Fix: Sensitive flags are filtered from argv before the payload is constructed. The webhook payload never includes credential-related flags.
---
F-13: CSV export vulnerable to formula injection
Severity: Medium Files: Reports and CSV export commands Threat: If a value from the Play API (e.g., an app name or reviewer text) starts with =, +, -, or @, spreadsheet applications treat it as a formula when the CSV is opened.
Fix: CSV fields are prefixed with ' when they start with a formula-triggering character. The apostrophe is interpreted by spreadsheets as a string escape, not rendered as content.
---
F-14: AI changelog prompt injection
Severity: Medium Files: packages/core/ (changelog generation, AI path) Threat: Commit messages or file paths containing LLM instruction syntax (e.g., "Ignore previous instructions and...") could alter the behavior of the AI prompt.
Fix: User-supplied content is wrapped in XML boundary tags before interpolation. The model sees the user content as a clearly bounded data block, not as instructions. This follows the same defense used in the Anthropic documentation for untrusted input.
---
F-15: Rate limiter bucket race condition
Severity: Low File: packages/api/src/rate-limiter.ts Threat: Multiple concurrent commands sharing a rate limiter instance could race on the same bucket, allowing combined burst throughput to exceed the per-minute quota limit.
Fix: Each bucket uses a promise-chain mutex. Acquire calls chain onto the previous acquire for the same bucket, serializing access within each bucket.
const mutexes = new Map<string, Promise<void>>();
// ...
const prev = mutexes.get(bucket) ?? Promise.resolve();
const next = prev.then(() => acquire());
mutexes.set(bucket, next);---
F-16: CI template exposes secrets at job level
Severity: Low Files: .github/workflows/ CI templates Threat: Defining env: GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SA_KEY }} at the job level exposes the secret to every step in that job, including any third-party actions.
Fix: Secrets are scoped to the individual steps that require them. Each step that calls a GPC command declares its own env: block. Steps that do not need the secret cannot access it.
---
Supply chain additions (v0.9.74)
pnpm.onlyBuiltDependencies
Added to package.json:
"pnpm": {
"onlyBuiltDependencies": ["turbo", "esbuild"]
}All other transitive dependencies are blocked from running install lifecycle hooks. Only turbo and esbuild (which require native compilation) are whitelisted.
--frozen-lockfile --ignore-scripts in all CI workflows
Every pnpm install call in CI now uses both flags. --frozen-lockfile prevents lock file drift. --ignore-scripts provides a second layer of install hook suppression at the pnpm level, in addition to the onlyBuiltDependencies whitelist.
deepsec CI job
Added as a separate deepsec: job in ci.yml. Runs on every push. Exports findings as a JSON artifact. Does not gate the build — findings require human triage before a fix is committed.
#!/usr/bin/env node
/**
* Detection script for GPC CLI.
* Returns JSON with installation status, version, auth state, and config.
* Used by Claude Code skill system for deterministic environment detection.
*
* Exit codes:
* 0 — GPC detected (may or may not be authenticated)
* 1 — GPC not found
*/
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
function run(cmd) {
try {
return execSync(cmd, { encoding: "utf-8", timeout: 10000 }).trim();
} catch {
return null;
}
}
const result = {
installed: false,
version: null,
installMethod: null,
authStatus: null,
authMethod: null,
profile: null,
envAuth: false,
defaultApp: null,
configFile: null,
nodeVersion: process.version,
};
// Check if gpc is installed globally
const versionOutput = run("gpc --version");
if (!versionOutput) {
// Try npx
const npxVersion = run("npx gpc --version 2>/dev/null");
if (!npxVersion) {
console.log(JSON.stringify(result, null, 2));
process.exit(1);
}
result.version = npxVersion;
result.installed = true;
result.installMethod = "npx";
} else {
result.version = versionOutput;
result.installed = true;
result.installMethod = "global";
}
// Check auth status
const authOutput = run("gpc auth status --json 2>/dev/null");
if (authOutput) {
try {
const auth = JSON.parse(authOutput);
result.authStatus = auth.status || "unknown";
result.authMethod = auth.method || null;
result.profile = auth.profile || null;
} catch {
result.authStatus = "parse_error";
}
}
// Check for env-based auth
if (process.env.GPC_SERVICE_ACCOUNT) {
result.envAuth = true;
}
// Check default app
const configOutput = run("gpc config get app --json 2>/dev/null");
if (configOutput) {
try {
const config = JSON.parse(configOutput);
result.defaultApp = config.value || config.app || null;
} catch {
result.defaultApp = configOutput || null;
}
}
// Check for .gpcrc.json in current directory
const rcPath = join(process.cwd(), ".gpcrc.json");
if (existsSync(rcPath)) {
result.configFile = rcPath;
}
console.log(JSON.stringify(result, null, 2));
process.exit(0);