Using Claude to automate Firestore security rule generation
Firestore security rules occupy an uncomfortable middle ground between a programming language and a configuration format. They are expressive enough to be complex, but restrictive enough that getting them right requires pattern memorisation and careful testing. A subtle mistake — confusing get() with exists(), or forgetting that resource.data refers to the existing document, not the incoming one — leads to either a security hole or a broken application.
LLMs are good at exactly this kind of domain-specific pattern generation. Given a clear description of your data model and access requirements, Claude can produce correct, well-structured security rules faster than most developers can write them by hand. The trick is in how you describe the problem and how you validate the output.
The prompt structure
Effective security rule generation requires three things in the prompt: your Firestore data schema, a description of who should have what access, and examples of edge cases to handle.
from anthropic import Anthropic
client = Anthropic()
def generate_security_rules(
schema_description: str,
access_requirements: str,
edge_cases: list[str] = None,
) -> str:
"""
Generate Firestore security rules from a natural-language description.
"""
edge_case_text = ""
if edge_cases:
edge_case_text = "\n\nEdge cases to handle:\n" + "\n".join(
f"- {ec}" for ec in edge_cases
)
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=3000,
system="""You are a Firebase security rules expert. Generate production-ready
Firestore security rules based on schema and access requirement descriptions.
Rules you must follow when generating security rules:
1. Always deny by default (rules_version = '2')
2. Use helper functions for repeated logic
3. Validate data types in write rules (request.resource.data)
4. Use `resource.data` for existing document data, `request.resource.data` for incoming
5. Include size limits on string fields to prevent abuse
6. Handle the case where documents may not yet exist (using `exists()`)
7. Comment each major rule with a one-line explanation
8. Test your logic: write rules should check both required fields exist AND optional fields have valid types if present
Return ONLY the security rules code, no explanation.""",
messages=[{
"role": "user",
"content": f"""Generate Firestore security rules for this data model:
SCHEMA:
{schema_description}
ACCESS REQUIREMENTS:
{access_requirements}
{edge_case_text}"""
}]
)
return response.content[0].text
A practical example
Here is a complete example for a multi-tenant blog platform:
schema = """
Collection: sites/{siteId}/articles/{articleId}
Fields:
- title: string (required, max 200 chars)
- content: string (required)
- status: string (enum: "draft", "pending", "published", "rejected")
- authorId: string (required, UID of the author)
- authorEmail: string
- tags: array of strings
- viewCount: number
- likeCount: number
- createdAt: timestamp
- publishedAt: timestamp (null if not published)
Collection: sites/{siteId}/users/{userId}
Fields:
- roles: array of strings (values: "admin", "reviewer", "author")
- authorName: string
- email: string
Collection: personas/{personaId}
Fields:
- authorSlug: string
- authorName: string
- specialties: array of strings
"""
access_requirements = """
1. Unauthenticated users: can only read articles where status == "published"
2. Authenticated users (authors):
- Can create articles (status must start as "draft", authorId must be their own UID)
- Can update their own articles only when status is "draft" (cannot change authorId or status to "published")
- Can read their own articles regardless of status
- Cannot delete articles
3. Reviewers (sites/{siteId}/users/{userId}.roles contains "reviewer"):
- Can read all articles
- Can update article status to "published" or "rejected"
- Cannot modify content, authorId, or other fields
4. Admins (sites/{siteId}/users/{userId}.roles contains "admin"):
- Full read/write access to articles
- Can read/write user documents
- Cannot modify their own roles (prevent privilege escalation)
5. viewCount and likeCount: can only be incremented, never set to arbitrary values by non-admins
6. personas collection: read by anyone authenticated, write only by admins
"""
edge_cases = [
"A user should not be able to set status to 'published' directly — only reviewers/admins can do that",
"Authors cannot change the authorId of an article after creation",
"Prevent admins from modifying their own roles array to prevent privilege escalation",
"viewCount increments must be positive (likeCount too)",
]
rules = generate_security_rules(schema, access_requirements, edge_cases)
print(rules)
Iterative refinement
The first generation is rarely final. Use follow-up prompts to refine:
def refine_rules(
current_rules: str,
refinement_request: str,
) -> str:
"""Refine existing rules based on new requirements or issues found."""
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=3000,
system="You are a Firebase security rules expert. Modify the provided "
"rules to incorporate the requested changes. Return only the "
"complete updated rules file, no explanation.",
messages=[
{
"role": "user",
"content": f"Here are the current rules:\n\n```\n{current_rules}\n```\n\n"
f"Please modify them to: {refinement_request}",
}
]
)
return response.content[0].text
Generating test cases
Rules are only as good as the tests that verify them. Ask Claude to generate test cases alongside the rules:
def generate_rule_tests(
rules: str,
schema_description: str,
) -> str:
"""
Generate Firebase Local Emulator Suite test cases for the security rules.
"""
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=3000,
system="""You are a Firebase testing expert. Generate comprehensive
security rule tests using the Firebase Testing SDK (@firebase/rules-unit-testing).
For each rule, generate at minimum:
- One test that ALLOWS a legitimate operation
- One test that DENIES an unauthorised operation
- Tests for boundary conditions (missing required fields, wrong types, etc.)
Use TypeScript. Structure tests with describe() blocks per collection.""",
messages=[{
"role": "user",
"content": f"""Generate test cases for these security rules:
RULES:
{rules}
SCHEMA:
{schema_description}"""
}]
)
return response.content[0].text
Validating rules before deployment
Before deploying generated rules, validate them syntactically and run them against your test suite:
import subprocess
import tempfile
import os
def validate_rules_syntax(rules: str) -> tuple[bool, str]:
"""
Use the Firebase CLI to validate security rules syntax.
Requires firebase-tools installed: npm install -g firebase-tools
"""
with tempfile.NamedTemporaryFile(
mode="w", suffix=".rules", delete=False
) as f:
f.write(rules)
temp_path = f.name
try:
result = subprocess.run(
["firebase", "firestore:rules:test", temp_path],
capture_output=True,
text=True,
timeout=30,
)
valid = result.returncode == 0
output = result.stdout + result.stderr
return valid, output
except subprocess.TimeoutExpired:
return False, "Validation timed out"
except FileNotFoundError:
return False, "firebase-tools not found; install with: npm install -g firebase-tools"
finally:
os.unlink(temp_path)
Keeping rules in sync with schema changes
As your Firestore schema evolves, security rules need to evolve too. Build a process for this:
def update_rules_for_schema_change(
current_rules: str,
old_schema: str,
new_schema: str,
change_description: str,
) -> str:
"""
Update security rules when the schema changes.
"""
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=3000,
system="You are a Firebase security rules expert. Update security rules "
"to reflect schema changes while preserving all existing access controls.",
messages=[{
"role": "user",
"content": f"""Update these security rules for a schema change.
CURRENT RULES:
{current_rules}
SCHEMA CHANGE:
{change_description}
OLD SCHEMA (relevant parts):
{old_schema}
NEW SCHEMA (relevant parts):
{new_schema}
Return the complete updated rules. Explain any non-obvious changes in comments."""
}]
)
return response.content[0].text
The most important practice when using AI-generated security rules is to never deploy without reading and understanding them. Claude generates correct rules most of the time, but Firestore security is a domain where a mistake is silent until exploited. Read every rule, run every test, and deploy to a staging environment before production.
Store your schema description and access requirements in a rules-spec.md file in your repository. When the spec changes, regenerate the rules, diff them against the current version, and review the diff. This keeps the generation process reproducible and makes changes auditable.
Explaining existing rules
AI generation is not only useful when writing rules from scratch. If you inherit an undocumented rules file, Claude can parse it and explain what each block actually does — which is often faster than reverse-engineering it yourself:
def explain_security_rules(rules: str) -> str:
"""
Generate a human-readable explanation of what each security rule does.
Useful for auditing inherited rules or onboarding new team members.
"""
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=2000,
system="""You are a Firebase security rules expert. Explain security rules
in plain English. For each match block:
1. State which collection or document it applies to
2. State who can read and under what conditions
3. State who can write (create/update/delete separately) and under what conditions
4. Call out any subtle logic — use of get(), exists(), auth claims, etc.
5. Flag any potential security issues or gaps
Be concise but complete. Use bullet points per collection.""",
messages=[{
"role": "user",
"content": f"Explain these Firestore security rules:\n\n```\n{rules}\n```",
}]
)
return response.content[0].text
def audit_rules_for_vulnerabilities(rules: str, schema: str) -> str:
"""
Ask Claude to specifically look for security vulnerabilities
in the provided rules.
"""
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=2000,
system="""You are a security auditor specialising in Firebase Firestore rules.
Review security rules and identify vulnerabilities. Look specifically for:
- Overly permissive read or write access
- Missing field validation (type, size, format)
- Logic errors in role checks (e.g., checking wrong document path for roles)
- Missing denial of document deletion where deletion should be blocked
- Race conditions in counter increments (viewCount, likeCount etc.)
- Users being able to escalate their own privileges
- Data that can be written by unauthenticated users unintentionally
For each issue: describe the vulnerability, explain the risk, and suggest a fix.""",
messages=[{
"role": "user",
"content": f"Audit these rules for security vulnerabilities:\n\nSCHEMA:\n{schema}\n\nRULES:\n```\n{rules}\n```",
}]
)
return response.content[0].text
The audit function is particularly valuable as a pre-deployment gate. Run it on every generated or modified rules file, review its findings, and incorporate any fixes before the deployment step. This creates a lightweight security review loop that would otherwise require a dedicated Firebase specialist.
Integrating into a CI/CD pipeline
Once you have generation, validation, and testing functions in place, the natural next step is wiring them into your CI pipeline. The following pattern runs on every pull request that touches the rules-spec.md file:
# scripts/regenerate_rules.py
import sys
import pathlib
import subprocess
def main():
spec_path = pathlib.Path("firestore/rules-spec.md")
rules_path = pathlib.Path("firestore/firestore.rules")
tests_path = pathlib.Path("firestore/rules.test.ts")
spec = spec_path.read_text()
# Parse sections from spec
sections = {}
current = None
for line in spec.splitlines():
if line.startswith("## "):
current = line[3:].strip().lower().replace(" ", "_")
sections[current] = []
elif current:
sections[current].append(line)
schema = "\n".join(sections.get("schema", []))
access = "\n".join(sections.get("access_requirements", []))
edge = sections.get("edge_cases", [])
print("Generating rules...")
rules = generate_security_rules(schema, access, edge)
print("Validating syntax...")
valid, output = validate_rules_syntax(rules)
if not valid:
print(f"Syntax validation failed:\n{output}", file=sys.stderr)
sys.exit(1)
print("Generating tests...")
tests = generate_rule_tests(rules, schema)
rules_path.write_text(rules)
tests_path.write_text(tests)
print("Running emulator tests...")
result = subprocess.run(
["npx", "jest", str(tests_path)],
capture_output=True, text=True
)
if result.returncode != 0:
print(result.stdout)
print(result.stderr, file=sys.stderr)
sys.exit(1)
print("All checks passed.")
if __name__ == "__main__":
main()
With this script in place, your GitHub Actions workflow becomes straightforward:
# .github/workflows/firestore-rules.yml
name: Firestore Rules
on:
pull_request:
paths:
- "firestore/rules-spec.md"
- "firestore/firestore.rules"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install anthropic
- run: npm install -g firebase-tools
- run: python scripts/regenerate_rules.py
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Commit regenerated rules if changed
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add firestore/firestore.rules firestore/rules.test.ts
git diff --cached --quiet || git commit -m "chore: regenerate Firestore rules from spec"
git push
This treats rules-spec.md as the source of truth. Engineers describe access requirements in plain English; the pipeline generates, validates, and tests rules automatically. The generated firestore.rules file is committed back to the branch, making the diff reviewable in the pull request alongside the spec change that triggered it.
Common failure modes and how to avoid them
After running this pattern across several projects, a few failure modes appear consistently:
Vague access descriptions produce vague rules. If you write "admins can do everything," the generated rules will be correct but will miss field-level constraints you probably intended. Be specific: "admins can read and write all fields except the roles field on their own user document."
Missing exists() checks cause broken apps. When a document has not been created yet, resource.data is null. Claude usually handles this correctly if you mention it in the system prompt, but verify that every create rule uses request.resource.data, not resource.data.
Helper functions get duplicated across collections. If your schema has multiple collections with similar role structures, Claude sometimes inlines the role check instead of factoring it into a reusable function. Mention this explicitly: "Use helper functions for any logic that applies to more than one collection."
Test generation lags behind rule generation. If you refine the rules without re-running the test generation, the tests go stale. Treat both as coupled outputs — regenerate together, always.
The practical impact of AI-assisted rule generation is significant: what once took a developer two to four hours of careful writing and manual testing can be reduced to a twenty-minute review of generated output. The human effort shifts from authoring to auditing, which is a more appropriate use of attention when security correctness is the goal.
Building a rules-spec.md that actually works
The quality of the generated rules depends almost entirely on the quality of the spec. A vague spec produces rules that technically compile but contain logical gaps. A few structural practices make a significant difference:
Name every actor explicitly. Instead of "logged-in users," write "authenticated users who are not owners of the resource," or "the document owner (request.auth.uid == resource.data.ownerId)." Vague actor descriptions produce vague role checks.
State what is NOT allowed as clearly as what is allowed. "Authors cannot delete articles" is easy to forget if you only enumerate what they can do. Listing prohibited operations gives Claude a complete picture and prevents it from generating permissive catch-all rules by accident.
Include the field type for every field. Firestore security rules can validate types using is string, is number, is timestamp, etc. If your spec does not mention field types, the generated rules will often skip type validation entirely, which leaves your schema open to unexpected data.
A minimal but well-structured spec section looks like this:
## Schema
Collection: teams/{teamId}/projects/{projectId}
Fields:
- name: string (required, max 100 chars)
- description: string (optional, max 500 chars)
- ownerId: string (required, must equal creator's auth.uid on create)
- memberIds: array of strings (UIDs)
- status: string (enum: "active", "archived")
- createdAt: timestamp (set on create, immutable after)
## Access requirements
- Unauthenticated: no access
- Team members (auth.uid in resource.data.memberIds): read only
- Project owner (auth.uid == resource.data.ownerId): read and update (cannot change ownerId or createdAt)
- Team admin (teams/{teamId}/members/{auth.uid}.role == "admin"): full access
- No one may delete projects — use status "archived" instead
## Prohibited operations
- Members cannot update the memberIds array (only admins can)
- Owners cannot transfer ownership by changing ownerId
- createdAt is immutable after document creation
This level of specificity produces rules that are correct on the first generation rather than requiring three or four refinement rounds.
When not to use AI-generated rules
AI generation is not appropriate in all situations. Rules that involve cross-document joins using get() to fetch a second document for role verification are prone to subtle errors because the path expressions are easy to get wrong and the consequences are difficult to spot in code review. For cross-collection role checks, write those rules by hand, document them explicitly, and add targeted unit tests that specifically exercise the role lookup path.
Similarly, if your rules involve rate limiting patterns (using Firestore to track request counts and enforcing limits in rules), generate a draft with Claude but treat it as a starting point that needs careful manual review — rate limiting logic in security rules is notoriously brittle.
The right mental model is to use Claude to handle the 80% of rules that follow predictable patterns (read if owner, write if role, validate required fields), and reserve manual authoring for the 20% that involve unusual logic, cross-collection lookups, or fine-grained field-level constraints that are hard to describe in prose.