Use Cases (Updated: 6/2/2026)

Web Security Headers with Claude Code: CSP, Nonces, HSTS, and Ad-Safe Deployment

Use Claude Code to implement CSP, nonces, HSTS, frame-ancestors, and security headers in modern web apps.

Web Security Headers with Claude Code: CSP, Nonces, HSTS, and Ad-Safe Deployment

Security headers are easy to underestimate because they are only strings on an HTTP response. In practice, they decide whether a browser may run injected scripts, embed your admin panel in an iframe, leak a full referrer URL to another site, or allow features such as camera and geolocation inside a page.

Claude Code can speed up the implementation, but it can also make a bad policy look polished if the prompt is too vague. A request like “fix my CSP errors” often leads to overly broad policies such as script-src * 'unsafe-inline' 'unsafe-eval'. That removes console noise, but it also removes most of the protection. The safer workflow is to inventory the site, ship Content-Security-Policy-Report-Only, collect reports, tighten the policy, and only then enforce it.

This guide covers a 2026-ready baseline for Content Security Policy (CSP), nonces, HSTS, X-Frame-Options, frame-ancestors, Referrer-Policy, and Permissions-Policy. It includes copyable examples for Next.js, Astro, Express, and Cloudflare Pages, plus CSP reports, Security Headers scans, CSP Evaluator review, and the common conflicts with Google Tag Manager, Google Analytics, AdSense, images, and CDNs. For the broader Claude Code safety model, pair this with Claude Code security best practices and Claude Code security audit.

Keep the primary docs open while adapting the code: MDN Content-Security-Policy, Next.js CSP guide, MDN Strict-Transport-Security, hstspreload.org, Cloudflare Pages headers, Helmet, Google Tag Manager CSP, and AdSense CSP guidance.

Start With the Inventory, Not the Header

Before asking Claude Code to edit anything, make it list what the site actually loads. This is the difference between an enforceable policy and a giant allowlist.

Review this repository and design web security headers.
Rules:
- Inventory external script, style, image, font, frame, and connect origins first.
- Start CSP in Report-Only mode.
- Avoid wildcard sources and permanent unsafe-inline.
- Explain whether Next.js nonces force dynamic rendering.
- Check Google Analytics, GTM, AdSense, image CDNs, and embedded video.
- Provide curl, Security Headers, and CSP Evaluator verification steps.

Ask for a table with script-src, style-src, img-src, connect-src, frame-src, and frame-ancestors. Those directives are often confused. frame-src controls what your page may embed. frame-ancestors controls who may embed your page. Google Analytics failures usually belong in connect-src, broken CDN images belong in img-src, and clickjacking protection belongs in frame-ancestors.

The practical baseline is:

HeaderRecommended starting pointProduction caution
Content-Security-PolicyBegin with Content-Security-Policy-Report-OnlyDo not hide problems with * or permanent unsafe-inline
Strict-Transport-SecurityStart short, for example max-age=300; includeSubDomainsAdd preload only after every subdomain is HTTPS-ready
X-Frame-OptionsDENY or SAMEORIGINUse CSP frame-ancestors for modern, granular control
Referrer-Policystrict-origin-when-cross-originDo not put secrets or personal data in URLs
Permissions-PolicyDisable unused browser featuresAllow only features your product truly uses
X-Content-Type-OptionsnosniffLow risk, usually worth setting everywhere

HSTS preload deserves special care. The preload list is useful, but it is not a day-one default. The official preload site recommends ramping up max-age in stages and warns that preloading can be hard to undo. If an old staging subdomain, email tracking domain, or customer vanity domain cannot serve HTTPS, includeSubDomains; preload can break real traffic.

How the CSP Rollout Should Flow

The most reliable path is observe, classify, then enforce.

flowchart LR
  A["Inventory origins"] --> B["Ship Report-Only CSP"]
  B --> C["Collect browser and report endpoint violations"]
  C --> D["Classify ads, analytics, CDN, embeds, and noise"]
  D --> E["Move to nonce or hash based enforcement"]
  E --> F["Verify with curl, Security Headers, and CSP Evaluator"]

Claude Code is useful at each step. It can find hardcoded third-party scripts, compare CSP reports with actual code, and write route-specific policies. It should not blindly add every reported domain. Browser extensions, corporate proxies, malicious probes, and old tags can all produce reports. Treat reports as evidence, not as a copy-paste allowlist.

Next.js Nonce-Based CSP

Next.js is where many teams get stuck. The current Next.js App Router documentation uses proxy.ts to generate a fresh nonce per request. Older projects may still use middleware.ts, but the idea is the same: generate an unpredictable value, put it in the request headers for the app to read, and put the matching nonce in the CSP.

Nonce-based CSP has a rendering cost. Static pages cannot reuse a single HTML file if the nonce must be different for every request. For public content sites, hash-based CSP or externalized scripts may be a better fit. For authenticated screens, checkout, admin, and account settings, per-request nonces are often worth it.

// proxy.ts
import { NextRequest, NextResponse } from "next/server";

export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const isDev = process.env.NODE_ENV !== "production";

  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${isDev ? "'unsafe-eval'" : ""} https: http:`,
    `style-src 'self' ${isDev ? "'unsafe-inline'" : `'nonce-${nonce}'`} https://fonts.googleapis.com`,
    "font-src 'self' https://fonts.gstatic.com",
    "img-src 'self' data: blob: https:",
    "connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com",
    "frame-src 'self' https://www.youtube-nocookie.com",
    "object-src 'none'",
    "base-uri 'self'",
    "form-action 'self'",
    "frame-ancestors 'none'",
    "upgrade-insecure-requests",
    "report-uri /api/csp-report",
  ]
    .join("; ")
    .replace(/\s{2,}/g, " ")
    .trim();

  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-nonce", nonce);

  const response = NextResponse.next({
    request: { headers: requestHeaders },
  });

  response.headers.set("Content-Security-Policy", csp);
  response.headers.set("X-Content-Type-Options", "nosniff");
  response.headers.set("X-Frame-Options", "DENY");
  response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
  response.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(self)");
  response.headers.set("Strict-Transport-Security", "max-age=300; includeSubDomains");

  return response;
}

export const config = {
  matcher: ["/((?!api/csp-report|_next/static|_next/image|favicon.ico).*)"],
};

When you use Google Tag Manager through Next.js, pass the nonce into the component. Google’s CSP guide recommends nonce support because the container bootstrap is inline JavaScript.

// app/layout.tsx
import { GoogleTagManager } from "@next/third-parties/google";
import { headers } from "next/headers";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const nonce = (await headers()).get("x-nonce") ?? undefined;

  return (
    <html lang="en">
      <body>
        {children}
        <GoogleTagManager gtmId="GTM-XXXXXXX" nonce={nonce} />
      </body>
    </html>
  );
}

The common mistake is keeping development exceptions in production. React development tooling may need unsafe-eval, and some styling setups may need unsafe-inline during migration. Those should be environment-specific, documented, and removed when the framework or library supports nonces.

CSP Reports and Report-Only Testing

Use a report endpoint before enforcing the policy. A minimal Next.js route handler can accept both older CSP report payloads and modern reporting payloads.

// app/api/csp-report/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  const contentType = request.headers.get("content-type") ?? "";
  const body = await request.text();

  const looksLikeCsp =
    contentType.includes("application/csp-report") ||
    contentType.includes("application/reports+json") ||
    body.includes("violated-directive");

  if (!looksLikeCsp) {
    return NextResponse.json({ ok: false }, { status: 415 });
  }

  console.warn("csp-report", body.slice(0, 4000));
  return new NextResponse(null, { status: 204 });
}

In a real product, store only what you need: document URL, violated directive, blocked URI, user agent, timestamp, and count. Do not store full query strings if they can contain personal data. report-uri is older, but it remains useful for compatibility. You can add report-to later, but do not depend on it as the only reporting path.

Astro, Express, and Cloudflare Examples

Astro middleware is a clean place to set headers for SSR routes. For mostly static sites, fixed headers and fewer inline scripts are often simpler than a nonce strategy.

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

const SECURITY_HEADERS: Record<string, string> = {
  "Content-Security-Policy-Report-Only": [
    "default-src 'self'",
    "script-src 'self' https://www.googletagmanager.com",
    "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
    "font-src 'self' https://fonts.gstatic.com",
    "img-src 'self' data: blob: https:",
    "connect-src 'self' https://www.google-analytics.com",
    "frame-src 'self' https://www.youtube-nocookie.com",
    "object-src 'none'",
    "base-uri 'self'",
    "form-action 'self'",
    "frame-ancestors 'none'",
    "report-uri /api/csp-report",
  ].join("; "),
  "X-Content-Type-Options": "nosniff",
  "X-Frame-Options": "DENY",
  "Referrer-Policy": "strict-origin-when-cross-origin",
  "Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=(self)",
};

export const onRequest = defineMiddleware(async (_context, next) => {
  const response = await next();
  for (const [name, value] of Object.entries(SECURITY_HEADERS)) {
    response.headers.set(name, value);
  }
  return response;
});

For Express, use Helmet, then configure CSP for your actual origins. Helmet’s defaults are helpful, but CSP is always application-specific.

// server.js
import crypto from "node:crypto";
import express from "express";
import helmet from "helmet";

const app = express();
const isDev = process.env.NODE_ENV !== "production";

app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString("base64");
  next();
});

app.use(
  helmet({
    contentSecurityPolicy: {
      useDefaults: false,
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`, "'strict-dynamic'", "https:", "http:", ...(isDev ? ["'unsafe-eval'"] : [])],
        styleSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`, "https://fonts.googleapis.com"],
        fontSrc: ["'self'", "https://fonts.gstatic.com"],
        imgSrc: ["'self'", "data:", "blob:", "https:"],
        connectSrc: ["'self'", "https://www.google-analytics.com", "https://analytics.google.com"],
        frameSrc: ["'self'", "https://www.youtube-nocookie.com"],
        objectSrc: ["'none'"],
        baseUri: ["'self'"],
        formAction: ["'self'"],
        frameAncestors: ["'none'"],
        reportUri: ["/csp-report"],
        upgradeInsecureRequests: [],
      },
    },
    strictTransportSecurity: { maxAge: 300, includeSubDomains: true },
    referrerPolicy: { policy: "strict-origin-when-cross-origin" },
    xFrameOptions: { action: "deny" },
  })
);

app.use((req, res, next) => {
  res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(self)");
  next();
});

app.listen(3000);

Cloudflare Pages can set static headers through _headers. It cannot generate per-request nonces in that static file, so use Workers/Functions or hash-based CSP when nonce enforcement is required.

# public/_headers
/*
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(self)
  Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https:; connect-src 'self' https://www.google-analytics.com; frame-src 'self' https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; report-uri /csp-report

Use Cases and Common Collisions

For a content site with Google Analytics, GTM, AdSense, image CDN, and YouTube embeds, CSP must protect the page without breaking revenue. GTM needs nonce handling for its inline bootstrap. AdSense’s own guidance supports strict CSP because ad-serving domains can change over time. GA4 usually needs script and connect permissions. Images often need a dedicated CDN source or a deliberately broad https: while you migrate.

For a SaaS admin area, reduce third-party scripts instead of allowing everything globally. Use frame-ancestors 'none', object-src 'none', base-uri 'self', and a narrow form-action. Payment SDKs and support widgets can be allowed only on the routes that need them.

For an embeddable widget, do not apply X-Frame-Options: DENY to the embed route. Use route-specific headers and put the trusted parent domains in frame-ancestors. This is the place where a perfect generic grade can be wrong for the product.

For a static marketing site on Cloudflare Pages, avoid inline JavaScript where possible. _headers is fast and reliable, but it is not a nonce generator. If the site needs strict CSP with nonces, move that surface to SSR, Workers, or a framework runtime.

Verification, CTA, and Field Result

Verify headers on at least three URLs: the home page, an authenticated or form page, and any embed or checkout route.

curl -I https://example.com/
curl -I https://example.com/login
curl -I https://example.com/embed/widget

Then use Security Headers for the broad header scan and CSP Evaluator for CSP-specific weaknesses. A high grade is useful, but it is not the same as a correct product policy. Review console errors, CSP reports, ad rendering, Analytics events, image loading, and iframe behavior before enforcing.

If this affects a production site, revenue surface, or shared team workflow, ClaudeCodeLab can review your repository-specific policy through Claude Code training and consultation. Teams that prefer self-service can start with ClaudeCodeLab products and templates and turn this checklist into a CLAUDE.md review rule.

In the test setup used for this article, the biggest win was shipping Report-Only first. It immediately separated GTM inline nonce issues from GA4 connect-src issues, YouTube frame-src issues, and CDN image issues. Asking Claude Code to classify the reports produced a better rollout plan than simply adding every blocked domain. Starting HSTS with max-age=300 also caught a forgotten subdomain before any preload decision could cause a long-lived outage.

#Claude Code #security #HTTP headers #CSP #web development
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.