I asked Claude Code to refactor our authentication module. Ten minutes later: clean code, proper error handling, tests included. The hype is real.
Then reality hit. I wanted to use Claude Code on actual production projects. Every real project has secrets:
.envfiles with database URLs and API keys- AWS/GCP/Azure credentials
- Stripe/payment provider keys
- OAuth client secrets
- Private SSL certificates
- Proprietary business logic (trading algorithms, pricing engines)
docker-compose.ymlwith production passwords- Firebase/Supabase config files
The naive approach — create a blank demo project, copy files over, ask Claude Code to edit them, copy back — is perfectly safe. It’s also perfectly miserable. I tried it for a week and wanted to throw my laptop out the window. So I tested four strategies over the last three months. Here’s what actually works.
What Claude Code Actually Sees
First, let’s clear up a common misconception. Claude Code doesn’t automatically scan your entire project. It requests file access. You approve or deny. This is good.
The problem is that the boundaries aren’t clean. When Claude Code needs architectural context, it naturally wants to read files near your sensitive code. The interface might be harmless, but the implementation contains database URLs, auth headers, and API keys.
Example:
# These files are SAFE for Claude Code to read:src/services/user.service.ts # TypeScript interfaceapp/api/users/route.ts # Next.js API routelib/repositories/user_repo.dart # Flutter repositorySources/Services/UserService.swift # iOS service protocol
# These files contain SECRETS — danger zone:.env # DATABASE_URL, API keysdocker-compose.yml # DB passwords, service tokenssrc/config/database.ts # Connection stringsfirebase.json # Firebase configgoogle-services.json # Android FirebaseGoogleService-Info.plist # iOS FirebaseHere’s what it looks like in code across multiple languages:
// src/services/payment.service.ts — harmless interface ✅export interface PaymentService { charge(amount: number, currency: string): Promise<PaymentResult>;}
// src/config/stripe.ts — contains secrets ❌export const stripeConfig = { secretKey: process.env.STRIPE_SECRET_KEY, // real key at runtime webhookSecret: "whsec_live_abc123...", // hardcoded! danger};# services/payment_service.py - harmless interface ✅class PaymentService: def charge(self, amount: float, currency: str) -> PaymentResult: pass
# config/stripe.py - contains secrets ❌STRIPE_SECRET_KEY = "sk_live_51J..." # dangerSTRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET", "whsec_live_abc123")// Services/PaymentService.swift - harmless protocol ✅protocol PaymentService { func charge(amount: Decimal, currency: String) async -> PaymentResult}
// Config/StripeConfig.swift - contains secrets ❌struct StripeConfig { static let secretKey = "sk_live_51J..." // danger static let webhookSecret = "whsec_live_abc123..."}Claude Code asks to read payment.service.ts for context. Seems fine. Then it asks for stripe.ts because that’s where configuration lives. Now it has your secrets.
The question isn’t whether Claude Code will see sensitive code. It’s when and how much you’re comfortable exposing. Here are four strategies I’ve used, ranked by security and daily sustainability.
Strategy 1: Demo Project + Copy-Paste
How it works:
Real Project (secret files) ↓ manual copyBlank Demo Project (no secrets) → Claude Code works here ↓ manual copy backReal Project (updated)Create a blank project with no secrets. Copy the files you want to edit into it. Use Claude Code there. Copy the edited files back to your real project.
The experience:
This is maximum security. Claude Code never touches your real project. The problem is that it’s miserable to use.
I tried this for a week. I needed Claude Code to refactor an auth module that depends on five services, two middleware files, and a database config. I copy-pasted seven files into the demo project. Claude Code asked for context about the base service class. I copy-pasted that. Then it needed the error handler. I copy-pasted that. Then it needed the middleware chain setup.
Thirty minutes later, I’d copy-pasted 15 files, Claude Code gave me a refactor that didn’t compile because it was missing context from three other files I didn’t copy, and I spent another 45 minutes manually merging changes back into the real project.
A task that should have taken five minutes took 90. The output quality was mediocre because Claude Code was working with context poverty — snippets without the full architectural picture.
Verdict:
| Security | Speed | Output Quality | Daily Sustainability |
|---|---|---|---|
| ⭐⭐⭐⭐⭐ | ⭐ | ⭐⭐ | ⭐ |
Best for: One-off edits on a single isolated file with zero dependencies. Not realistic for most real work.
Strategy 2: Duplicate Project + Git Patch
How it works:
git clone real-project duplicated-projectcd duplicated-project./remove-secrets.sh # automated script./add-dummy-secrets.sh # automated script# work with Claude Code heregit diff > changes.patchcd ../real-projectgit apply changes.patchClone your real project. Run a script that removes all secret files and replaces constants with dummy values. Work with Claude Code in this clean duplicate. When done, generate a git patch of the changes and apply it to the real project.
The experience:
This was a massive improvement over copy-paste. I wrote a universal cleanup script:
#!/bin/bash# remove-secrets.sh (works for any project)
# Remove environment filesrm -f .env .env.local .env.production .env.*.local
# Remove infrastructure secretsrm -f docker-compose.prod.yml docker-compose.override.yml
# Remove cloud credentialsrm -f **/credentials.json **/service-account.jsonrm -rf ~/.aws/ .aws/
# Remove certificates and keysfind . -name "*.pem" -deletefind . -name "*.key" -deletefind . -name "*.p12" -deletefind . -name "*.keystore" -deletefind . -name "*.jks" -delete
# Remove platform-specific secretsfind . -name "google-services.json" -deletefind . -name "GoogleService-Info.plist" -deleterm -f local.properties local.settings.json
# Replace hardcoded secrets with dummies (adapt patterns to your stack)# TypeScript/JavaScriptfind . -name "*.ts" -o -name "*.js" | xargs sed -i '' 's/sk_live_[a-zA-Z0-9]*/sk_test_DUMMY_KEY/g'find . -name "*.ts" -o -name "*.js" | xargs sed -i '' 's/https:\/\/api\.internal\./https:\/\/api.example.com\//g'
# Pythonfind . -name "*.py" | xargs sed -i '' 's/sk_live_[a-zA-Z0-9]*/sk_test_DUMMY_KEY/g'
# Gofind . -name "*.go" | xargs sed -i '' 's/sk_live_[a-zA-Z0-9]*/sk_test_DUMMY_KEY/g'
echo "✅ Secrets removed"#!/bin/bash# Create dummy .envcat > .env << EOFDATABASE_URL=postgres://user:pass@localhost:5432/mydbREDIS_URL=redis://localhost:6379STRIPE_SECRET_KEY=sk_test_DUMMYSTRIPE_WEBHOOK_SECRET=whsec_test_DUMMYJWT_SECRET=dummy-secret-for-devAWS_ACCESS_KEY_ID=AKIADUMMYAWS_SECRET_ACCESS_KEY=dummyGOOGLE_OAUTH_CLIENT_SECRET=GOCSPX-dummyEOF
# Create dummy docker-compose.ymlcat > docker-compose.yml << EOFversion: '3.8'services: db: image: postgres:15 environment: POSTGRES_PASSWORD: dummy POSTGRES_DB: mydbEOF
# Platform-specific dummies# Androidif [ -d "android" ]; then echo "STRIPE_KEY=pk_test_DUMMY" > local.properties echo '{"project_info":{"project_id":"dummy"}}' > google-services.jsonfi
# iOSif [ -d "ios" ]; then echo '<?xml version="1.0" encoding="UTF-8"?><plist><dict><key>API_KEY</key><string>DUMMY</string></dict></plist>' > ios/Runner/GoogleService-Info.plistfi
echo "✅ Dummy secrets added"Now Claude Code has near-full project context without seeing real secrets. The output quality jumped dramatically. It understood the architecture, followed existing patterns, and generated code that actually compiled.
The sync workflow was fast — about 30 seconds per iteration:
git diff > /tmp/changes.patchcd ../real-projectgit apply /tmp/changes.patchnpm test # or: pytest, go test, ./gradlew test, swift test# if tests fail, iterateThe downside is maintenance. Every time a new secret is added to the real project, I need to update remove-secrets.sh. Every time a new required configuration file is added, I need to update add-dummy-secrets.sh. It’s friction, but manageable.
Verdict:
| Security | Speed | Output Quality | Daily Sustainability |
|---|---|---|---|
| ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
Best for: Medium-to-high security environments where you can invest 15-20 minutes in initial setup and accept occasional script updates.
Strategy 3: .claudeignore on Real Project
How it works:
Real Project/├── .claudeignore # blocks sensitive files├── secrets/ # ❌ blocked│ └── *.pem├── .env # ❌ blocked├── docker-compose.yml # ❌ blocked└── src/ # ✅ accessibleWork directly on your real project. Use a .claudeignore file to block Claude Code from accessing sensitive files and directories.
The experience:
This is the fastest strategy by far. Zero overhead. No duplicate projects. No sync scripts. No context loss. Claude Code sees your full architecture and produces excellent output.
Here’s a comprehensive .claudeignore that works for any stack:
# ============================================# Universal Secrets# ============================================.env**.pem*.key*.p12*.keystore*.jkscredentials.jsonservice-account.json**/secrets/**
# ============================================# Infrastructure# ============================================docker-compose.prod.ymldocker-compose.override.yml**/vault/**secrets.yamlvault-config.hcl
# ============================================# Platform-Specific Secrets# ============================================
# Androidgoogle-services.jsonlocal.propertieskeystore.properties
# iOSGoogleService-Info.plist*.mobileprovision*.certSigningRequest
# Firebasefirebase.json.firebaserc
# Cloud Providers.aws/.gcp/.azure/
# Node.js.npmrc
# Python.pypirc
# ============================================# Proprietary Business Logic# ============================================src/**/trading/src/**/pricing-engine/src/**/encryption/impl/lib/**/proprietary/**/internal/algorithms/
# ============================================# Build Artifacts (not secret but noisy)# ============================================node_modules/dist/build/.next/out/__pycache__/*.pyctarget/bin/obj/.gradle/.dart_tool/Pods/Podfile.lockvendor/.terraform/The output quality is the best of all strategies because Claude Code has complete architectural context. It follows your patterns perfectly, understands your dependency injection setup, and generates code that compiles on the first try.
The trust question:
Here’s the honest part: .claudeignore is a software boundary, not a physical one. The files are still on your disk. You’re trusting that:
- Claude Code respects
.claudeignoreand doesn’t read blocked files - There are no bugs that accidentally bypass the filter
- No files that reference secrets (like a README with example API calls) slip through
I’ve used this strategy for months on personal projects and client work where I control the risk policy. I’ve never seen Claude Code access a blocked file. But I can’t verify it in real-time. There’s no audit log showing “Claude Code requested X, was denied.”
Some companies can’t accept this. If your security policy requires provable guarantees that secrets were never accessible, this isn’t enough.
Edge cases:
Files that reference secrets but don’t contain them directly:
# docs/API.md - not a secret file, but contains secret references## Authentication
curl -H "Authorization: Bearer $STRIPE_KEY" \ https://api.stripe.com/v1/customersYou need to either add docs/*.md to .claudeignore (loses valuable documentation context) or manually review every file that might reference secrets.
Which secrets matter for which platform:
| Secret File | Platforms |
|---|---|
.env, .env.* | All (universal) |
docker-compose.prod.yml | Backend, fullstack |
google-services.json | Android |
GoogleService-Info.plist | iOS |
credentials.json | GCP (any) |
*.keystore, *.jks | Android, Java |
*.p12, *.pem | iOS, SSL certificates |
local.properties | Android |
secrets.yaml | Kubernetes |
.npmrc | Node.js private registries |
.pypirc | Python private packages |
firebase.json | Firebase (all platforms) |
Verdict:
| Security | Speed | Output Quality | Daily Sustainability |
|---|---|---|---|
| ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Best for: Solo developers, startups, and teams with moderate security requirements who trust software boundaries and can accept that blocked files are still on disk.
Strategy 4: Git Worktree + Clean Branch
How it works:
real-project/ # main worktree on 'master' (has secrets)real-project-clean/ # second worktree on 'clean' branch (no secrets) # Claude Code works here ↑# Sync via git merge or cherry-pickUse git worktree to create a second working directory of the same repository on a separate branch. The clean branch has all secrets removed and added to .gitignore. Claude Code works in the clean worktree. You sync changes between branches using git merge or git cherry-pick.
The experience:
This is the most elegant solution I’ve found. It combines the physical security of Strategy 2 (secrets literally don’t exist in the Claude Code workspace) with git-native sync (no manual patches).
Setup (one-time, ~10 minutes):
cd ~/projects/my-app
# Create clean branchgit checkout -b clean
# Remove all secretsrm -f .env .env.local .env.productionrm -f docker-compose.prod.ymlrm -f **/credentials.json **/service-account.jsonfind . -name "*.pem" -deletefind . -name "*.key" -deletefind . -name "*.p12" -deletefind . -name "google-services.json" -deletefind . -name "GoogleService-Info.plist" -delete
# Add dummy secretsecho "DATABASE_URL=postgres://user:pass@localhost:5432/mydb" > .envecho "STRIPE_KEY=sk_test_DUMMY" >> .envecho "JWT_SECRET=dummy-secret-for-dev" >> .env
git add -Agit commit -m "Remove secrets for AI workspace"
# Create second worktreegit worktree add ../my-app-clean clean
# Add real secrets to .gitignore on clean branch (prevents accidents)cat >> .gitignore << EOF# Real secrets (never commit on clean branch).env.productiondocker-compose.prod.yml**/credentials.json**/service-account.json*.pem*.key*.p12google-services.jsonGoogleService-Info.plistEOF
git add .gitignoregit commit -m "Ignore secrets on clean branch"Daily workflow:
cd ~/projects/my-app-clean # Claude Code workspace# work with Claude Codegit add -Agit commit -m "Add retry logic to payment service"
cd ~/projects/my-app # real projectgit cherry-pick clean # pull changes from clean branchnpm test # or: pytest, go test, cargo test, swift test# verify with real secretsThe beauty: Claude Code’s changes are committed to the clean branch. You review them, test them in the real workspace, and cherry-pick them over. If something breaks, you haven’t polluted your main branch.
Why this works:
- Physical security: The clean worktree doesn’t have secret files on disk. Even if Claude Code tries to read them, they don’t exist.
- Full context: The clean branch has your full architecture, dependencies, and patterns — just with dummy secrets.
- Git-native sync: No manual patches.
git cherry-pickorgit mergehandles the sync in seconds. - Verifiable: You can
lsthe clean workspace and confirm secret files aren’t there.
The learning curve:
Most developers don’t know about git worktree. It’s intimidating the first time. But once you’ve set it up, it’s incredibly smooth. I now use this for every production project.
Verdict:
| Security | Speed | Output Quality | Daily Sustainability |
|---|---|---|---|
| ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
Best for: Teams with strong security policies who need provable guarantees, developers comfortable with git, and anyone working on long-term projects where setup time amortizes to zero.
Which Strategy Should You Use?
Choose based on your risk profile:
| If you need… | Use Strategy |
|---|---|
| Absolute maximum security, don’t care about speed | #1 Demo Project |
| Strong security + good context, can handle setup | #2 Duplicate + Patch |
| Maximum speed + great output, trust software boundaries | #3 .claudeignore |
| Physical security + git workflow, comfortable with git | #4 Git Worktree |
My personal journey:
Started with #1. Lasted a week. Switched to #2 for two months. Now I use #4 for production work and #3 for personal projects.
Bonus: The Single Biggest Quality Improvement
Regardless of which strategy you use, add a CLAUDE_CONTEXT.md file to your project root. This is the single biggest improvement to output quality I’ve found.
Examples for different stacks:
Next.js/React
## Project: E-commerce Dashboard
**Tech Stack:**- Next.js 14 + TypeScript + App Router- React Server Components + Server Actions- Prisma + PostgreSQL- TailwindCSS + shadcn/ui
**Architecture:**app/ ├── (auth)/ # Auth routes ├── api/ # API routes └── dashboard/ # Protected routes lib/ ├── actions/ # Server Actions ├── db/ # Prisma client └── utils/ # Shared utilities
**Critical Conventions:**- Server Components by default, use "use client" only when needed- All database access via Prisma, never raw SQL- Server Actions for mutations, not API routes- All secrets via process.env, never hardcoded
**Secret Files (for reference, never access):**- `.env.local` - DATABASE_URL, STRIPE_SECRET_KEY- `docker-compose.yml` - Database passwords
**When working on payment features:**Reference `app/api/stripe/webhook/route.ts` as the canonical pattern.Python/FastAPI
## Project: Analytics API
**Tech Stack:**- FastAPI + Python 3.11- SQLAlchemy + Alembic- PostgreSQL + Redis- Pydantic for validation
**Architecture:**app/ ├── api/ # Routers ├── core/ # Config, dependencies ├── models/ # SQLAlchemy models ├── schemas/ # Pydantic schemas ├── services/ # Business logic └── repositories/ # Data access
**Critical Conventions:**- Router → Service → Repository pattern (never skip layers)- All config via Settings (Pydantic BaseSettings), never direct env access- Async everywhere (async def, await)- Type hints mandatory (mypy strict mode)
**Secret Files:**- `.env` - DATABASE_URL, REDIS_URL, SECRET_KEY- `docker-compose.yml` - Service passwords
**When working on authentication:**Reference `app/services/auth_service.py` for token patterns.Mobile (Kotlin Multiplatform)
## Project: Mobile Banking App (KMP)
**Tech Stack:**- Kotlin Multiplatform (Android + iOS)- Compose Multiplatform for UI- Ktor for networking- SQLDelight for database- Koin for DI
**Architecture:**shared/ ├── commonMain/ # Shared logic ├── androidMain/ # Android-specific └── iosMain/ # iOS-specific androidApp/ # Android UI iosApp/ # iOS UI
**Critical Conventions:**- Business logic in commonMain, platform-specific only when necessary- Sealed classes for all state (Loading, Success, Error)- expect/actual for platform APIs- All secrets injected via platform modules
**Secret Files:**- Android: `local.properties`, `google-services.json`- iOS: `GoogleService-Info.plist`, `Config.xcconfig`
**When working on network layer:**Reference `shared/src/commonMain/.../ApiClient.kt` for patterns.With this file in place, Claude Code’s output quality jumps 3-5x. It knows your patterns, follows your architecture, and doesn’t make assumptions about where to put code.
I wrote a deep-dive on this: Why CLAUDE.md Is the Most Important File in Your Project.
The Bottom Line
You don’t have to choose between Claude Code’s productivity and your project’s security. Pick the strategy that matches your risk tolerance:
- Strategy #1: Maximum security, terrible speed (demo project)
- Strategy #2: Strong security, good balance (duplicate + patch)
- Strategy #3: Maximum speed, trust required (.claudeignore)
- Strategy #4: Physical security + git workflow (worktree)
Whatever you choose, add CLAUDE_CONTEXT.md. That alone will 3x your output quality.
I started with copy-paste hell, moved to duplicate projects, and now run git worktrees for production and .claudeignore for personal work. The right strategy depends on your project, your team, and your security requirements.
Want to go deeper? The Claude Code Mastery course covers all of this and more — including the complete security module (Phase 2) with hands-on exercises. Phases 1-3 are free.
Get the free Claude Code Cheat Sheet — 50+ commands in a single PDF — when you join the newsletter.