Claude Code with Azure: Bicep, Functions, RBAC Workflow
Using Claude Code on Azure projects
Azure is a platform where Claude Code can move fast and where it can also move fast in the wrong direction. The surface area is broad, the tooling has a long history, and Microsoft Learn examples often demonstrate patterns at a scope wider than production work tolerates. Bicep, ARM, azd, App Service, Functions, Container Apps, Key Vault, Cosmos DB, Storage, Service Bus, Event Grid, Application Insights, Entra ID, RBAC. Claude has training data on all of it, but the data is messy. Bicep syntax has changed several times since launch, ARM JSON conventions are deeply outdated in many tutorials, and Azure CLI flags rotate every few releases. The result is that an unprompted Claude Code session generates code that compiles, deploys, and quietly drops you into the wrong scope or the wrong API version.
Without a project-specific CLAUDE.md, Claude invents Bicep resource types that do not exist, mixes Bicep with ARM JSON in the same module, generates RBAC assignments scoped to the subscription when they should be scoped to a single resource, and writes Function handlers that bypass Key Vault and read secrets straight from environment variables. Worse, it can suggest az commands that target the wrong subscription and apply changes you cannot reverse. None of these failures look obviously wrong on first read, which is why they make it through review on a fast-moving project.
This guide covers the CLAUDE.md patterns that prevent those failures. The configuration shown below is what an experienced Azure engineer would gravitate to, encoded as rules Claude can apply consistently across every session, every refactor, and every new feature branch. If you are new to Claude Code, the Claude Code setup guide covers installation. For the foundational concepts, the CLAUDE.md explained guide walks through the file format. The Claude Code with AWS guide covers the equivalent patterns for AWS projects, and many of the rules transfer directly because the failure modes are platform-agnostic, the names of the offending resources just change.
The pattern across this guide is consistent. Each section pairs a CLAUDE.md rule block with the runtime artefact it produces, whether that is a Bicep module, an Azure CLI command, or a TypeScript Function handler. The rules are short, declarative, and written in the imperative voice Claude responds to most reliably. Every rule that uses the word NEVER is a hard constraint, every rule that uses ASK is a directive to pause and request human input rather than guess. That two-mode contract is the lever that turns Claude from a fast generator into a fast generator that asks the right questions.
The Azure CLAUDE.md template, Bicep, and the IaC posture
The CLAUDE.md at your project root is read before every Claude Code session. For an Azure project, it needs to declare which IaC tool you use, which subscription and resource group are in scope, the RBAC rules that govern every role assignment Claude writes, and the Key Vault patterns that govern how secrets reach your application code. The first half of the file is stack and structure, the second half is hard constraints that are non-negotiable.
# Azure project rules
## Stack
- IaC: Bicep (preferred for new infrastructure)
- Orchestration: azd (Azure Developer CLI), one azure.yaml per environment
- Legacy: ARM JSON templates, read-only unless explicitly migrating
- Compute: Azure Functions (TypeScript, programming model v4) and App Service
- Data: Cosmos DB (SQL API), Azure SQL only where relational guarantees are required
- Secrets: Key Vault, accessed via managed identity, never via connection strings in env vars
- Identity: Entra ID, all access via managed identity or workload identity federation
- Region default: uksouth
- Subscription: dev = 11111111-1111-1111-1111-111111111111, prod = 22222222-2222-2222-2222-222222222222
- Set via `az account set --subscription` before any deploy command
## Project structure
- infra/: Bicep modules and main.bicep
- infra/main.bicep: subscription-scoped entry, calls per-resource modules
- infra/modules/: reusable Bicep modules (one resource per file)
- infra/abbreviations.json: official Azure resource abbreviations (CAF aligned)
- src/functions/: Function App source, one folder per function trigger
- src/lib/: shared TypeScript modules used by handlers
- src/web/: App Service applications
- test/: unit tests + Bicep what-if assertions
- scripts/: operational shell scripts (never auto-run in CI)
- azure.yaml: azd configuration
## Naming and tagging
- Resource names: ${abbreviation}-${workload}-${env}-${region}-${instance}
- All resources tag: workload, environment, owner, costCentre, managedBy
- Function Apps: func-${workload}-${env}
- Storage accounts: st${workload}${env}${region}${instance} (lowercase, no hyphens, max 24 chars)
- Key Vaults: kv-${workload}-${env}-${region}
- Cosmos DB: cosmos-${workload}-${env}
## RBAC rules (HARD)
- NEVER assign Owner or Contributor at subscription scope
- NEVER assign User Access Administrator outside platform-team subscriptions
- Role assignments scope to the smallest viable container: resource > resource group > subscription
- Role assignments use built-in role definition IDs, never role names in production code
- Application identities are managed identities (system or user-assigned), never service principals with secrets
- If a needed permission is unknown, ASK before assigning a broader role to make it work
## Deploy rules
- `az deployment group what-if` before any `az deployment group create`
- `azd provision --preview` before `azd provision`
- Production deploys require manual approval. Claude never runs `az deployment` or `azd up` against the prod subscription
- All destructive commands (az group delete, az resource delete, az cosmosdb delete) are denied for Claude
## Bicep conventions
- One resource per module file, parameterised inputs and outputs declared at the top
- All modules use the latest API version pinned explicitly, never @latest
- Soft-delete and purge-protection enabled on all Key Vaults
- All storage accounts: TLS 1.2 minimum, public blob access disabled, network ACLs default deny
- All databases: backup retention >= 7 days
Three rules in this CLAUDE.md prevent the most common Claude Code failures with Azure. The no Owner or Contributor at subscription scope rule is the most consequential. Microsoft Learn examples often demonstrate role assignment at subscription level for clarity, and Claude reproduces those examples literally. The result is an application identity with the ability to delete the entire subscription, which is exactly the access that ends up reviewed by an auditor six months later. The pin API versions rule prevents Bicep modules that drift between deploys. Claude has training data spanning multiple API versions for every resource type, and without an explicit pin it picks one that may be deprecated, missing properties you need, or behaving differently from what the docs imply. The ask-do-not-guess rule is the antidote to a specific failure mode. Bicep has hundreds of resource types and thousands of properties. Claude will sometimes invent plausible-sounding properties that do not exist. The deploy fails with InvalidTemplateDeployment. Telling Claude to ask converts a silent fabrication into a question.
The choice of which IaC tool owns which boundary deserves to live in CLAUDE.md too. Bicep is the modern Azure DSL. It transpiles to ARM JSON, has resource-aware tooling in VS Code, and is what Microsoft's templates target now. New infrastructure should be written in Bicep modules, one resource per file, with explicit parameters and outputs. azd (Azure Developer CLI) wraps Bicep with environment management, app source bundling, and deploy orchestration through azure.yaml. ARM JSON remains the lowest-level format, sometimes still required for legacy parity or features not yet landed in Bicep. The CLAUDE.md rule "ARM is read-only unless migrating" stops Claude from generating raw ARM when Bicep would suffice.
// infra/modules/keyvault.bicep
@description('Key Vault name, must be globally unique')
param name string
@description('Azure region')
param location string = resourceGroup().location
@description('Tags applied to the vault')
param tags object
@description('Tenant ID for the vault')
param tenantId string = subscription().tenantId
resource kv 'Microsoft.KeyVault/vaults@2024-04-01-preview' = {
name: name
location: location
tags: tags
properties: {
tenantId: tenantId
sku: { family: 'A', name: 'standard' }
enableRbacAuthorization: true
enableSoftDelete: true
softDeleteRetentionInDays: 90
enablePurgeProtection: true
publicNetworkAccess: 'Disabled'
networkAcls: {
defaultAction: 'Deny'
bypass: 'AzureServices'
}
}
}
output id string = kv.id
output name string = kv.name
output uri string = kv.properties.vaultUri
The pinned API version at the top of the resource declaration is the single most useful constraint Claude Code obeys. Without it, Claude picks an API version from training data and the resulting module fails to deploy on any subscription with a different default. The enableRbacAuthorization flag forces RBAC for data-plane access, which deletes the older access-policy model that Claude will otherwise generate by default. Once that flag is on, the assignment of Key Vault Secrets User to your Function's managed identity becomes the only access path, and the security model is consistent end-to-end.
# azure.yaml
name: orders-platform
metadata:
template: azd-init@1.10.0
services:
api:
project: ./src/functions/orders-api
language: ts
host: function
web:
project: ./src/web
language: ts
host: appservice
hooks:
preprovision:
shell: sh
run: ./scripts/preprovision.sh
postdeploy:
shell: sh
run: ./scripts/postdeploy-smoke.sh
Claude generates correct Bicep more reliably than ARM JSON, partly because the Bicep compiler catches errors before deploy and partly because the syntax is denser and Claude is less likely to drift mid-template. azd handles the orchestration around it. ARM stays read-only.
Functions, RBAC, Key Vault, and Cosmos DB
Functions is where most Azure compute time gets written and where Claude Code is most useful when configured. Handler structure, binding patterns, identity configuration, and local development all belong in CLAUDE.md, alongside the RBAC and secrets patterns that determine whether a Function App runs as a least-privilege citizen or as a foothold into the rest of the subscription.
## Functions conventions
### Handler structure (src/functions/${name}/index.ts), TypeScript v4 model
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
import { ordersService } from '../../lib/services/orders';
export async function ordersApi(req: HttpRequest, ctx: InvocationContext): Promise<HttpResponseInit> {
ctx.log('orders-api invoked', { invocationId: ctx.invocationId });
try {
const body = await req.json();
const result = await ordersService.process(body);
return { status: 200, jsonBody: result };
} catch (err) {
ctx.error('orders-api failed', { error: err });
throw err;
}
}
app.http('ordersApi', {
methods: ['POST'],
authLevel: 'function',
handler: ordersApi,
});
### Idempotency
- Every queue, event, or service-bus triggered handler must be idempotent
- Use a deterministic dedup key from the message envelope
- Persist dedup state in Cosmos DB with TTL = 24 hours
### Identity and configuration
- All Functions run under managed identity, system-assigned by default
- Secrets reference Key Vault: @Microsoft.KeyVault(SecretUri=...)
- Connection strings (Cosmos, Service Bus) use identity-based connections, not key-based
- Application Insights connection: APPLICATIONINSIGHTS_CONNECTION_STRING from infra output
### Cold start
- Module-scope: imports, SDK clients, config loading
- Handler-scope: business logic only
- Node runtime: 22, Premium plan for latency-sensitive workloads, Consumption otherwise
- Bundle target: < 5MB for Consumption plan cold start
The TypeScript v4 programming model is the default for new Functions work. Claude often defaults to v3 patterns from older training data, registering functions through function.json files instead of app.http. The explicit declaration in CLAUDE.md keeps generated handlers in the modern model. The identity-based connections rule is where Function App security usually breaks. By default, Functions ship with AzureWebJobsStorage set to a key-based connection string in app settings. The correct production pattern is AzureWebJobsStorage__accountName plus a managed identity with Storage Blob Data Owner and Storage Queue Data Contributor on the storage account. Claude generates the key-based version by default and only switches when CLAUDE.md states the rule explicitly. The module-scope versus handler-scope distinction is the biggest cold start optimisation Claude can apply, and it does so consistently when the rule is explicit. Without it, Claude instantiates SDK clients inside the handler, producing a fresh client on every cold start.
RBAC is where Azure security is won or lost. A correct design assigns role definitions at the smallest viable scope, uses managed identities for application access, and routes secrets through Key Vault rather than app settings. Claude can produce RBAC that meets this bar with explicit pattern guidance.
// infra/modules/rbac.bicep
@description('Function App principal ID (managed identity)')
param functionAppPrincipalId string
@description('Cosmos DB account resource ID, scope target')
param cosmosAccountId string
@description('Key Vault resource ID, scope target')
param keyVaultId string
// Built-in role definition IDs, never role names
var cosmosDbDataContributor = '00000000-0000-0000-0000-000000000002'
var keyVaultSecretsUser = '4633458b-17de-408a-b874-0445c86b69e6'
// Cosmos DB Built-in Data Contributor, scoped to the database account, not subscription
resource cosmosRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(cosmosAccountId, functionAppPrincipalId, cosmosDbDataContributor)
scope: resourceGroup()
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cosmosDbDataContributor)
principalId: functionAppPrincipalId
principalType: 'ServicePrincipal'
}
}
// Key Vault Secrets User, scoped to the vault resource, not the resource group
resource kvRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(keyVaultId, functionAppPrincipalId, keyVaultSecretsUser)
scope: resourceGroup()
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', keyVaultSecretsUser)
principalId: functionAppPrincipalId
principalType: 'ServicePrincipal'
}
}
The guid() deterministic name is the standard idiom for role assignments in Bicep. It guarantees the assignment name is stable across deploys, which means the assignment is updated rather than recreated when other parameters change. Claude omits this when the rule is implicit and produces role assignments with random names that drift on each deploy. The built-in role definition IDs are stable across the Azure platform. Using IDs rather than role names is the difference between a deploy that works on every tenant and one that fails because the local language pack renames roles in the portal.
For everyday operational tasks, the Azure CLI examples follow the same scope-narrow pattern.
# Assign Key Vault Secrets User to a Function App's managed identity, vault-scoped only
PRINCIPAL_ID=$(az functionapp identity show \
--name func-orders-prod \
--resource-group rg-orders-prod \
--query principalId -o tsv)
VAULT_ID=$(az keyvault show \
--name kv-orders-prod-uksouth \
--resource-group rg-orders-prod \
--query id -o tsv)
az role assignment create \
--assignee-object-id "$PRINCIPAL_ID" \
--assignee-principal-type ServicePrincipal \
--role "Key Vault Secrets User" \
--scope "$VAULT_ID"
# Verify least privilege: should return exactly one role at vault scope
az role assignment list \
--assignee "$PRINCIPAL_ID" \
--scope "$VAULT_ID" \
--output table
# App Service: store the Key Vault reference, not the secret itself
az webapp config appsettings set \
--name app-orders-prod \
--resource-group rg-orders-prod \
--settings "COSMOS_CONNECTION=@Microsoft.KeyVault(SecretUri=https://kv-orders-prod-uksouth.vault.azure.net/secrets/cosmos-connection/)"
# Function App, same pattern
az functionapp config appsettings set \
--name func-orders-prod \
--resource-group rg-orders-prod \
--settings "STRIPE_SECRET=@Microsoft.KeyVault(SecretUri=https://kv-orders-prod-uksouth.vault.azure.net/secrets/stripe-secret/)"
The --assignee-object-id plus --assignee-principal-type ServicePrincipal pattern avoids the silent fallback where --assignee resolves a managed identity by display name and matches the wrong object. Claude reaches for the simpler --assignee form by default and only switches when CLAUDE.md flags the difference. The @Microsoft.KeyVault(SecretUri=...) syntax tells Azure to resolve the secret at app-start using the host's managed identity, which means the secret never appears in deployment logs, az CLI output, or developer machines. Claude generates the literal-string form by default and switches to references when the CLAUDE.md rule is explicit.
Cosmos DB SQL API is the default Azure NoSQL store for new work. The Bicep side enables RBAC data-plane access by setting disableLocalAuth: true, which forces every client into managed identity authentication.
// infra/modules/cosmos.bicep, abbreviated
resource account 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' = {
name: name
location: location
tags: tags
kind: 'GlobalDocumentDB'
properties: {
databaseAccountOfferType: 'Standard'
consistencyPolicy: { defaultConsistencyLevel: 'Session' }
locations: [{ locationName: location, failoverPriority: 0, isZoneRedundant: false }]
capabilities: [{ name: 'EnableServerless' }]
publicNetworkAccess: 'Disabled'
disableLocalAuth: true
backupPolicy: {
type: 'Continuous'
continuousModeProperties: { tier: 'Continuous7Days' }
}
}
}
Without disableLocalAuth: true, Cosmos still issues primary and secondary keys, and Claude reaches for those keys in application code by default. Once local auth is disabled, the only path is managed identity plus the Cosmos DB Built-in Data Contributor role, which CLAUDE.md already mandates above.
// src/lib/services/orders.ts
import { CosmosClient } from '@azure/cosmos';
import { DefaultAzureCredential } from '@azure/identity';
const endpoint = requireEnv('COSMOS_ENDPOINT');
const credential = new DefaultAzureCredential();
const client = new CosmosClient({ endpoint, aadCredentials: credential });
const ordersContainer = client.database('orders').container('orders');
export const ordersService = {
async getOrder(orderId: string, customerId: string) {
const { resource } = await ordersContainer
.item(orderId, customerId)
.read();
return resource;
},
async putOrder(order: { id: string; customerId: string; items: unknown[]; total: number }) {
await ordersContainer.items.create(order, { disableAutomaticIdGeneration: true });
},
async listForCustomer(customerId: string) {
const { resources } = await ordersContainer.items
.query({
query: 'SELECT * FROM c WHERE c.customerId = @customerId',
parameters: [{ name: '@customerId', value: customerId }],
})
.fetchAll();
return resources;
},
};
function requireEnv(name: string): string {
const v = process.env[name];
if (!v) throw new Error(`Required env var not set: ${name}`);
return v;
}
The DefaultAzureCredential chain handles managed identity in production, Azure CLI credentials locally, and workload identity federation in CI. Claude defaults to passing a primary key to CosmosClient constructor, which works against the local emulator and fails against any production account that has disableLocalAuth enforced. The requireEnv helper fails fast at module load instead of producing a runtime error halfway through processing the first request. The parameterised query with @customerId rather than string concatenation prevents NoSQL injection. Cosmos accepts both forms, and Claude defaults to string concatenation when the rule is implicit. For deeper TypeScript patterns that improve handler quality, the Claude Code TypeScript guide covers strict mode, type-only imports, and discriminated unions.
Deploy and preview workflow with the Azure CLI
Claude Code has its own permission system separate from Azure RBAC. The .claude/settings.local.json controls which Bash commands Claude can run autonomously. For Azure projects, this is where you stop Claude from running production deploys, deleting resource groups, or rotating keys. The allow list lets Claude do real work, the deny list stops the operations that should require human approval, and Bicep what-if plus azd preview become the standard pre-flight checks.
{
"permissions": {
"allow": [
"Bash(npm test*)",
"Bash(npm run build*)",
"Bash(az login*)",
"Bash(az account show*)",
"Bash(az account set --subscription dev*)",
"Bash(az group show*)",
"Bash(az deployment group what-if*)",
"Bash(az deployment group create --resource-group rg-orders-dev*)",
"Bash(azd provision --preview*)",
"Bash(azd provision -e dev*)",
"Bash(azd deploy -e dev*)",
"Bash(az functionapp invoke --name func-orders-dev*)",
"Bash(az role assignment list*)"
],
"deny": [
"Bash(az account set --subscription prod*)",
"Bash(az deployment group create --resource-group rg-orders-prod*)",
"Bash(azd provision -e prod*)",
"Bash(azd deploy -e prod*)",
"Bash(az group delete*)",
"Bash(az resource delete*)",
"Bash(az cosmosdb delete*)",
"Bash(az keyvault delete*)",
"Bash(az keyvault purge*)",
"Bash(az role assignment delete*)",
"Bash(az role definition delete*)",
"Bash(az ad sp create-for-rbac*)"
]
}
}
The deny list deserves a closer look. Production subscription switching is blocked because az account set is an implicit-state command, and a Claude Code session that flips into the prod subscription will run every subsequent command there until you notice. Resource group deletion is blocked because az group delete is the single most destructive command in the Azure CLI, capable of removing entire applications in one call. Key Vault delete and purge are blocked separately because purge bypasses soft-delete and is irreversible. The az ad sp create-for-rbac block is intentional, that command issues long-lived secrets that bypass the managed identity convention, and accepting it as a one-line shortcut is how organisations end up with secret-rotation backlogs that take quarters to clear.
The standard Azure deploy workflow with these guardrails looks predictable. Claude is allowed to run az deployment group what-if against the dev resource group, which shows exactly what will change before any change is applied. The output is a structured diff: resources to create, resources to update, resources to ignore. Claude reads the diff, summarises it, and either proceeds with az deployment group create against dev or pauses for human review. For azd-managed deployments the equivalent is azd provision --preview followed by azd provision -e dev, with the production environment behind explicit human approval. Application code deploys through azd deploy -e dev against the same gated environment.
# Dev deploy workflow, every step Claude is allowed to take
az account set --subscription dev
az deployment group what-if \
--resource-group rg-orders-dev \
--template-file infra/main.bicep \
--parameters @infra/dev.parameters.json
az deployment group create \
--resource-group rg-orders-dev \
--template-file infra/main.bicep \
--parameters @infra/dev.parameters.json \
--confirm-with-what-if
azd deploy -e dev --service api
azd deploy -e dev --service web
# Smoke test against dev
az functionapp invoke \
--name func-orders-dev \
--resource-group rg-orders-dev \
--function-name ordersApi \
--data @test/events/smoke.json
The --confirm-with-what-if flag on az deployment group create runs the diff again immediately before the apply and asks for confirmation. Claude reads the diff and either proceeds, asks for human review, or declines if any change matches a forbidden pattern such as a public endpoint flip, an RBAC assignment at subscription scope, or a Key Vault property change that would disable purge protection. The smoke-test invocation at the end is a tight feedback loop: deploy, invoke, read the response, confirm or roll back. For longer-running services, the same pattern extends to Application Insights query checks against the freshly-deployed environment, asserting that error rates remain at baseline before the deploy is considered complete.
Real Azure work is rarely a single resource group. Most production projects span Bicep for the cloud infrastructure, a TypeScript or Next.js frontend deployed to App Service or Static Web Apps, a separate observability stack, and shared TypeScript packages consumed by both Functions and the web tier. The Claude Code monorepo guide covers the workspace patterns that let Claude navigate this kind of multi-service repository without getting lost. The same workspace settings that make Claude reliable across packages also make it reliable across environment boundaries: a single source-of-truth for the dev subscription IDs, a single CLAUDE.md, and a single permission allow-list shared across every service folder. Drift between services is one of the leading causes of "the deploy worked yesterday" incidents, and a flat workspace structure with shared rules is the cheapest defence against it.
Application Insights and Log Analytics deserve their own paragraph in CLAUDE.md when the project relies on them. Claude generates correct KQL queries for Application Insights when the schema is described, and it generates incorrect queries when the schema is implicit. A short rule block listing the table names in scope (requests, dependencies, exceptions, customMetrics, customEvents), the standard where timestamp > ago(1h) lower bound, and the project's preferred aggregation interval is enough to keep generated queries operational rather than aspirational. Claude will otherwise reach for older Azure Monitor schema that no longer applies to App Insights workspaces deployed in the last two years.
Hard rules and final guardrails
A short list of non-negotiable rules belongs at the bottom of every Azure CLAUDE.md. These are the rules Claude must never violate regardless of what the user asks for, and they become the most-cited section of the file as the project grows. Claude can hold ten constraints in mind and apply them consistently. Twenty become probabilistic. The rules below cover the failure modes that produce real Azure incidents.
## Hard rules
1. NEVER assign Owner or Contributor at subscription scope to an application identity.
2. NEVER use access policies on Key Vault, RBAC authorization only.
3. NEVER store secrets in app settings as literal strings, use @Microsoft.KeyVault references.
4. NEVER set disableLocalAuth=false on production Cosmos DB or storage accounts.
5. NEVER deploy to a production subscription without explicit user approval per deploy.
6. NEVER run az deployment, azd up, or azd provision against prod without preview.
7. NEVER use @latest API versions in Bicep, pin every API version explicitly.
8. NEVER write a Function handler that mutates state without idempotency.
9. NEVER hardcode subscription IDs, tenant IDs, or resource IDs, use subscription(), tenant(), and resourceId().
10. If a Bicep resource type, RBAC role definition, or az CLI flag is uncertain, ASK before generating code that depends on it.
The "ask if uncertain" rule deserves emphasis. Bicep's resource catalogue runs into the thousands and Claude's training has gaps, especially around recently-released services and recent API versions. The honest behaviour, when Claude does not know whether Microsoft.Web/sites/config@2024-04-01 exposes a property called keyVaultReferenceIdentity or keyVaultIdentity, is to ask. CLAUDE.md is where you make the honest behaviour explicit. The configuration in this guide produces a development environment where RBAC is least-privilege, Bicep modules are pinned to specific API versions, Function handlers run under managed identity with idempotency, secrets reach the runtime through Key Vault references, and deploys are gated against production damage. The result is Claude generating Azure code at the level of a careful senior engineer, not a junior with subscription Owner access.
Claude Code performs at the level of context you give it. Without CLAUDE.md it invents resource types, generates Owner-scoped role assignments, ships Functions that read keys directly, and writes Cosmos clients that bypass managed identity. With the configuration above it follows your conventions, asks when uncertain, and lets you focus on the parts of Azure engineering that need a human: capacity planning, incident review, regional failover design, the architectural calls that no amount of training data substitutes for. The Claude Code best practices guide covers principles across project types. Claudify includes an Azure-specific CLAUDE.md template, pre-configured for Bicep modules, RBAC least-privilege, Key Vault references, and managed identity patterns.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify