Skip to main content
hooksSource-backed
Cloudflare logo

Cloudflare Wrangler Config Guard Hook for Claude Code

Read-only Claude Code PostToolUse hook that checks edited Cloudflare Wrangler config files for required Worker fields, environment inheritance traps, secret-like vars, deprecated Workers Sites usage, and source-of-truth drift before Claude continues.

by oktofeesh1·added 2026-06-04·
Trigger:PostToolUse
Review first review before installing

Open the source and read safety notes before installing.

Citation facts

Source-backed facts for citing this resource, derived directly from the registry — also available as plain text for AI assistants.

Source URLs
https://developers.cloudflare.com/workers/wrangler/configuration/, https://github.com/cloudflare/workers-sdk
Brand
Cloudflare
Brand domain
cloudflare.com
Brand asset source
brandfetch
Safety notes
This hook is static and read-only. It reads only non-symlinked Wrangler config files inside the current project and does not run `wrangler deploy`, `wrangler dev`, `wrangler whoami`, `wrangler secret list`, package scripts, build plugins, or network commands., The hook exits non-zero only for parse failures or missing required top-level Worker fields such as `name` and `compatibility_date`; warnings remain advisory so the user can review them., The TOML checks are heuristic and do not replace Wrangler's own parser or a trusted pre-deploy dry run. Use them as a fast edit-time guard, then run official Wrangler validation in a trusted repository., Do not expand this hook into a dry-run deploy hook unless the team has reviewed local build-script execution, inherited environment variables, and output-directory cleanup., Keep matchers scoped to config file edits. Running config checks after every tool call adds noise without improving Cloudflare safety.
Privacy notes
The hook reads `wrangler.toml`, `wrangler.json`, or `wrangler.jsonc` only inside the current project, with symlinks rejected and a 1 MiB size cap., Wrangler config files can contain worker names, route patterns, account identifiers, resource IDs, binding names, environment names, analytics settings, and non-secret vars., Hook output may include file paths, config key names, environment names, and secret-like variable names, so avoid pasting terminal logs into public issue comments without review., If a Wrangler config currently contains real secrets in `vars`, the hook can reveal the key names and the surrounding configuration context. Move sensitive values to Wrangler secrets before sharing output.
Author
oktofeesh1
Submitted by
oktofeesh1
Claim status
unclaimed
Last verified
2026-06-04

Decision playbook

Review trust signals before you adopt

Signals are present but mixed. Use the checklist below to confirm the source and operational safety for your environment.

Compare context
Selected

0

Current score

78

Baseline

Delta

No baseline selected

No major trust-signal divergence detected in the current selection.

Source and provenance checks

Complete

Confirm ownership and provenance before trusting install instructions.

  • Source link availableRequired

    Open the canonical repository and verify ownership.

    Done
  • Source provenance statusRequired

    Marked as source-backed.

    Done
  • Metadata reviewed

    Registry metadata indicates a reviewed listing.

    Done

Safety and privacy checks

Complete

Validate risk disclosures before installation or API wiring.

  • Safety notes presentRequired

    Review the listed safety guidance before running commands.

    Done
  • Privacy notes presentRequired

    Review data handling notes before connecting accounts or secrets.

    Done
  • Trust level risk gateRequired

    Trust level does not block evaluation.

    Done

Package and install checks

Needs review

Check package metadata and artifact integrity signals.

  • Install payload available

    Install or copy payload is available for review.

    Done
  • Package verification flag

    No package verification flag provided.

    Pending
  • Checksum metadata

    No checksum provided for downloaded artifact.

    Pending

Compare-driven decision checks

Needs review

Use compare context to validate trade-offs before adoption.

  • Compare tray has multiple entries

    Add at least one more entry to compare trust differences.

    Pending
  • Baseline comparison available

    No baseline peer selected yet.

    Pending
  • Diverging trust signals identified

    No major trust-signal divergence found.

    Pending

Setup at a glance

CLI install

Copy-ready — paste the snippet to get started.

Adoption plan

Balanced adoption plan

Current risk score 16/100. Use staged verification before broader rollout.

Risk 16

Pre-adoption checks

Validate source and review signals before any execution.

  • Confirm source provenanceRequired

    Source URL/provenance metadata is present.

    Done
  • Confirm metadata review state

    Listing has review metadata.

    Done
  • Verify install payload

    Install/config payload exists and can be inspected.

    Done

Security checks

Confirm safety, privacy, and package integrity signals.

  • Review safety notesRequired

    Safety notes are present.

    Done
  • Review privacy notesRequired

    Privacy notes are present.

    Done
  • Verify package integrity metadata

    No package verification/checksum metadata.

    Pending

Rollout

Adopt in controlled steps based on the selected plan.

  • Run in isolated sandbox firstRequired

    Use a constrained sandbox and observe behavior across multiple tasks.

    Pending
  • Roll out graduallyRequired

    Roll out to a small cohort before wider usage.

    Pending
  • Set monitoring and fallback

    Define rollback path and monitor errors after adoption.

    Pending

Evidence readiness

Evidence readiness matrix · balanced

Required evidence gates are covered (5/6 signals complete).

Risk 15

Source provenance

Present

Source repository/provenance is listed.

Required in this preset

Metadata review

Present

Review metadata is present.

Required in this preset

Safety notes

Present

Safety notes are present.

Required in this preset

Privacy notes

Present

Privacy notes are present.

Optional in this preset

Package integrity

Missing

Package integrity metadata is missing.

Optional in this preset

Install payload

Present

Install payload is available.

Required in this preset

Required evidence gates are covered for this preset.

Decision timeline

Decision timeline · balanced

5/6 steps complete with no blocking gaps for this preset.

Risk 14

triage

Confirm source provenanceRequired

Source/provenance metadata is available.

Done

triage

Check metadata review statusRequired

Review metadata is available.

Done

verify

Review safety notesRequired

Safety notes are available.

Done

verify

Review privacy notes

Privacy notes are available.

Done

verify

Validate package integrity metadata

Package integrity metadata is missing.

Pending

rollout

Verify install payload and commandsRequired

Install payload is available.

Done

No required blockers for this timeline preset.

Prerequisite readiness

Prerequisite readiness

5 prerequisites to line up before setup. Includes a review or approval gate.

0/5 ready
Install & runtime1Configuration1Review & approval1General2

Safety & privacy surface

Safety & privacy surface

5 safety and 4 privacy notes across 5 risk areas. Review closely: credentials & tokens, permissions & scopes.

5 areas
  • SafetyCredentials & tokensThis hook is static and read-only. It reads only non-symlinked Wrangler config files inside the current project and does not run `wrangler deploy`, `wrangler dev`, `wrangler whoami`, `wrangler secret list`, package scripts, build plugins, or network commands.
  • SafetyGeneralThe hook exits non-zero only for parse failures or missing required top-level Worker fields such as `name` and `compatibility_date`; warnings remain advisory so the user can review them.
  • SafetyExecution & processesThe TOML checks are heuristic and do not replace Wrangler's own parser or a trusted pre-deploy dry run. Use them as a fast edit-time guard, then run official Wrangler validation in a trusted repository.
  • SafetyLocal filesDo not expand this hook into a dry-run deploy hook unless the team has reviewed local build-script execution, inherited environment variables, and output-directory cleanup.
  • SafetyPermissions & scopesKeep matchers scoped to config file edits. Running config checks after every tool call adds noise without improving Cloudflare safety.
  • PrivacyGeneralThe hook reads `wrangler.toml`, `wrangler.json`, or `wrangler.jsonc` only inside the current project, with symlinks rejected and a 1 MiB size cap.
  • PrivacyCredentials & tokensWrangler config files can contain worker names, route patterns, account identifiers, resource IDs, binding names, environment names, analytics settings, and non-secret vars.
  • PrivacyCredentials & tokensHook output may include file paths, config key names, environment names, and secret-like variable names, so avoid pasting terminal logs into public issue comments without review.
  • PrivacyCredentials & tokensIf a Wrangler config currently contains real secrets in `vars`, the hook can reveal the key names and the surrounding configuration context. Move sensitive values to Wrangler secrets before sharing output.

Safety notes

  • This hook is static and read-only. It reads only non-symlinked Wrangler config files inside the current project and does not run `wrangler deploy`, `wrangler dev`, `wrangler whoami`, `wrangler secret list`, package scripts, build plugins, or network commands.
  • The hook exits non-zero only for parse failures or missing required top-level Worker fields such as `name` and `compatibility_date`; warnings remain advisory so the user can review them.
  • The TOML checks are heuristic and do not replace Wrangler's own parser or a trusted pre-deploy dry run. Use them as a fast edit-time guard, then run official Wrangler validation in a trusted repository.
  • Do not expand this hook into a dry-run deploy hook unless the team has reviewed local build-script execution, inherited environment variables, and output-directory cleanup.
  • Keep matchers scoped to config file edits. Running config checks after every tool call adds noise without improving Cloudflare safety.

Privacy notes

  • The hook reads `wrangler.toml`, `wrangler.json`, or `wrangler.jsonc` only inside the current project, with symlinks rejected and a 1 MiB size cap.
  • Wrangler config files can contain worker names, route patterns, account identifiers, resource IDs, binding names, environment names, analytics settings, and non-secret vars.
  • Hook output may include file paths, config key names, environment names, and secret-like variable names, so avoid pasting terminal logs into public issue comments without review.
  • If a Wrangler config currently contains real secrets in `vars`, the hook can reveal the key names and the surrounding configuration context. Move sensitive values to Wrangler secrets before sharing output.

Prerequisites

  • Claude Code project where hooks are allowed by user or project policy.
  • Cloudflare Workers or Pages project with `wrangler.toml`, `wrangler.json`, or `wrangler.jsonc` checked into the repository.
  • `jq` available locally to parse Claude Code hook input.
  • Node.js available locally for static JSON/JSONC/TOML heuristics.
  • A reviewed `.claude/settings.json` or user settings hook configuration scoped to `Write`, `Edit`, and `MultiEdit`.

Schema details

Install type
cli
Reading time
9 min
Difficulty score
68
Troubleshooting
Yes
Breaking changes
No
Source repository stats
Scope
Source repo
Runtime and command metadata
Trigger
PostToolUse
Script language
bash
Script body
#!/usr/bin/env bash
set -uo pipefail

input="$(cat)"

if ! command -v jq >/dev/null 2>&1; then
  echo "Wrangler config guard skipped: jq is required to parse Claude Code hook input." >&2
  exit 0
fi

if ! command -v node >/dev/null 2>&1; then
  echo "Wrangler config guard skipped: Node.js is required for static config checks." >&2
  exit 0
fi

tool_name="$(printf '%s' "$input" | jq -r '.tool_name // .toolName // empty')"
file_path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.path // .toolInput.file_path // .toolInput.path // empty')"

case "$tool_name" in
  Write|Edit|MultiEdit|write|edit|multiedit) ;;
  *) exit 0 ;;
esac

case "$(basename "$file_path")" in
  wrangler.toml|wrangler.json|wrangler.jsonc) ;;
  *) exit 0 ;;
esac

if [ -z "$file_path" ] || [ ! -f "$file_path" ]; then
  exit 0
fi

if [ -L "$file_path" ]; then
  echo "Wrangler config guard blocked: config path must not be a symlink." >&2
  exit 1
fi

WRANGLER_CONFIG_PATH="$file_path" node <<'NODE'
const fs = require("node:fs");
const pathModule = require("node:path");
const path = process.env.WRANGLER_CONFIG_PATH;
const name = path.split(/[\\/]/).pop();
const maxConfigBytes = 1024 * 1024;
const errors = [];
const warnings = [];

const stats = fs.lstatSync(path);
if (stats.isSymbolicLink()) {
  errors.push("Config path must not be a symlink.");
}

const realPath = fs.realpathSync(path);
const projectRoot = fs.realpathSync(process.cwd());
const relativePath = pathModule.relative(projectRoot, realPath);
if (relativePath.startsWith("..") || pathModule.isAbsolute(relativePath)) {
  errors.push("Config path must stay within the current project.");
}

if (stats.size > maxConfigBytes) {
  errors.push("Config file is too large to inspect safely.");
}

if (errors.length) {
  console.error(`Wrangler config guard: checking ${name}`);
  for (const error of errors) console.error(`[error] ${error}`);
  process.exit(1);
}

const raw = fs.readFileSync(path, "utf8");

function displayName(value) {
  return JSON.stringify(String(value)).replace(/[\u007f-\u009f]/g, (char) => {
    return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
  });
}

function stripJsonc(value) {
  let out = "";
  let inString = false;
  let quote = "";
  let escaped = false;
  let lineComment = false;
  let blockComment = false;

  for (let i = 0; i < value.length; i += 1) {
    const char = value[i];
    const next = value[i + 1];

    if (lineComment) {
      if (char === "\n") {
        lineComment = false;
        out += char;
      }
      continue;
    }

    if (blockComment) {
      if (char === "*" && next === "/") {
        blockComment = false;
        i += 1;
      }
      continue;
    }

    if (inString) {
      out += char;
      if (escaped) {
        escaped = false;
      } else if (char === "\\") {
        escaped = true;
      } else if (char === quote) {
        inString = false;
      }
      continue;
    }

    if (char === "\"" || char === "'") {
      inString = true;
      quote = char;
      out += char;
      continue;
    }

    if (char === "/" && next === "/") {
      lineComment = true;
      i += 1;
      continue;
    }

    if (char === "/" && next === "*") {
      blockComment = true;
      i += 1;
      continue;
    }

    out += char;
  }

  return out.replace(/,\s*([}\]])/g, "$1");
}

function looksSecretLike(key) {
  return /(secret|token|password|private|credential|api[_-]?key|client[_-]?secret)/i.test(key);
}

function checkConfigObject(config) {
  if (!config || typeof config !== "object" || Array.isArray(config)) {
    errors.push("Config did not parse to an object.");
    return;
  }

  if (!config.name) errors.push("Missing top-level `name`.");
  if (!config.compatibility_date) errors.push("Missing top-level `compatibility_date`.");

  if (!config.main && !config.assets && !config.pages_build_output_dir) {
    warnings.push("No `main`, `assets`, or `pages_build_output_dir` found. `main` is optional only for assets-only Workers.");
  }

  if (config.site) {
    warnings.push("`site` is present. Workers Sites is deprecated in Wrangler v4; prefer Workers Static Assets for new projects.");
  }

  if (config.send_metrics !== false) {
    warnings.push("`send_metrics` is not set to false. Review Cloudflare telemetry expectations for this repository.");
  }

  if (config.vars && typeof config.vars === "object") {
    for (const key of Object.keys(config.vars)) {
      if (looksSecretLike(key)) {
        warnings.push(`Top-level vars contains secret-like key ${displayName(key)}; use Wrangler secrets for sensitive values.`);
      }
    }
  }

  const bindingKeys = [
    "vars",
    "kv_namespaces",
    "r2_buckets",
    "d1_databases",
    "queues",
    "durable_objects",
    "services",
    "analytics_engine_datasets",
    "vectorize",
    "ai",
  ];

  if (config.env && typeof config.env === "object") {
    for (const [envName, envConfig] of Object.entries(config.env)) {
      if (!envConfig || typeof envConfig !== "object") continue;

      for (const key of bindingKeys) {
        if (config[key] && !Object.prototype.hasOwnProperty.call(envConfig, key)) {
          warnings.push(`Environment ${displayName(envName)} does not define ${displayName(key)}; Wrangler treats bindings and vars as non-inheritable.`);
        }
      }

      if (envConfig.vars && typeof envConfig.vars === "object") {
        for (const key of Object.keys(envConfig.vars)) {
          if (looksSecretLike(key)) {
            warnings.push(`Environment ${displayName(envName)} vars contains secret-like key ${displayName(key)}; use Wrangler secrets for sensitive values.`);
          }
        }
      }
    }
  }
}

function stripTomlComment(line) {
  let out = "";
  let inString = false;
  let quote = "";
  let escaped = false;

  for (const char of line) {
    if (inString) {
      out += char;
      if (escaped) {
        escaped = false;
      } else if (char === "\\") {
        escaped = true;
      } else if (char === quote) {
        inString = false;
      }
      continue;
    }

    if (char === "\"" || char === "'") {
      inString = true;
      quote = char;
      out += char;
      continue;
    }

    if (char === "#") break;
    out += char;
  }

  return out.trim();
}

function checkToml() {
  const top = new Set();
  const sections = new Set();
  const envSections = new Set();
  const envKeys = new Map();
  const bindingKeys = [
    "vars",
    "kv_namespaces",
    "r2_buckets",
    "d1_databases",
    "queues",
    "durable_objects",
    "services",
    "analytics_engine_datasets",
    "vectorize",
    "ai",
  ];
  let section = "";

  for (const originalLine of raw.split(/\r?\n/)) {
    const line = stripTomlComment(originalLine);
    if (!line) continue;

    const sectionMatch = line.match(/^\s*\[+\s*([A-Za-z0-9_.-]+)\s*\]+/);
    if (sectionMatch) {
      section = sectionMatch[1];
      sections.add(section);
      const envMatch = section.match(/^env\.([A-Za-z0-9_-]+)(?:\.([A-Za-z0-9_.-]+))?/);
      if (envMatch) {
        envSections.add(envMatch[1]);
        if (!envKeys.has(envMatch[1])) envKeys.set(envMatch[1], new Set());
        if (envMatch[2]) envKeys.get(envMatch[1]).add(envMatch[2].split(".")[0]);
      }
      continue;
    }

    const keyMatch = line.match(/^([A-Za-z0-9_$-]+)\s*=/);
    if (!keyMatch) continue;

    const key = keyMatch[1];
    if (!section) {
      top.add(key);
    } else if (section === "vars" && looksSecretLike(key)) {
      warnings.push(`Top-level vars contains secret-like key ${displayName(key)}; use Wrangler secrets for sensitive values.`);
    } else {
      const envVarMatch = section.match(/^env\.([A-Za-z0-9_-]+)\.vars$/);
      if (envVarMatch && looksSecretLike(key)) {
        warnings.push(`Environment ${displayName(envVarMatch[1])} vars contains secret-like key ${displayName(key)}; use Wrangler secrets for sensitive values.`);
      }
    }
  }

  if (!top.has("name")) errors.push("Missing top-level `name`.");
  if (!top.has("compatibility_date")) errors.push("Missing top-level `compatibility_date`.");
  if (!top.has("main") && !sections.has("assets") && !top.has("pages_build_output_dir")) {
    warnings.push("No `main`, `[assets]`, or `pages_build_output_dir` found. `main` is optional only for assets-only Workers.");
  }
  if (sections.has("site")) {
    warnings.push("`site` is present. Workers Sites is deprecated in Wrangler v4; prefer Workers Static Assets for new projects.");
  }
  if (!top.has("send_metrics")) {
    warnings.push("`send_metrics` is not set. Review Cloudflare telemetry expectations for this repository.");
  }

  for (const envName of envSections) {
    const keys = envKeys.get(envName) || new Set();
    for (const key of bindingKeys) {
      if (sections.has(key) && !keys.has(key)) {
        warnings.push(`Environment ${displayName(envName)} does not define ${displayName(key)}; Wrangler treats bindings and vars as non-inheritable.`);
      }
    }
  }
}

console.error(`Wrangler config guard: checking ${name}`);

try {
  if (name === "wrangler.json" || name === "wrangler.jsonc") {
    checkConfigObject(JSON.parse(stripJsonc(raw)));
  } else {
    checkToml();
  }
} catch {
  errors.push("Parse failed: review the Wrangler config syntax before continuing.");
}

for (const warning of warnings) console.error(`[warn] ${warning}`);
for (const error of errors) console.error(`[error] ${error}`);

if (!warnings.length && !errors.length) {
  console.error("Wrangler config guard: no issues found.");
}

process.exit(errors.length ? 1 : 0);
NODE
Full copyable content
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/wrangler-config-guard.sh"
          }
        ]
      }
    ]
  }
}

About this resource

Overview

This hook checks Cloudflare Wrangler config files immediately after Claude Code edits them. It focuses on static issues that are easy to miss during AI-assisted config changes:

  • Required top-level Worker fields such as name and compatibility_date.
  • Missing entrypoint/static-asset hints when neither main, assets, nor pages_build_output_dir is present.
  • Environment binding drift for vars, KV, R2, D1, queues, Durable Objects, services, analytics datasets, Vectorize, and Workers AI.
  • Secret-like keys placed in vars instead of Wrangler secrets.
  • Deprecated Workers Sites site configuration.
  • Project telemetry review via send_metrics.

It deliberately does not run Wrangler. That keeps the hook useful for reviewing untrusted or partially reviewed config edits, because wrangler deploy --dry-run can still execute local build commands and bundler plugins.

Requirements

  • Claude Code hooks enabled in user or project settings.
  • A Cloudflare Workers or Pages project with a Wrangler config file.
  • jq installed locally.
  • Node.js installed locally.
  • A reviewed hook configuration scoped to Write, Edit, and MultiEdit.

Hook Configuration

Save the hook executable outside the project workspace, then add the hook command to your user-level Claude Code settings file. Do not point PostToolUse hooks at scripts stored inside the project, because Claude can edit those files before the hook command runs:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/wrangler-config-guard.sh"
          }
        ]
      }
    ]
  }
}

Hook Script

Save this as $HOME/.claude/hooks/wrangler-config-guard.sh and make it executable. Keep the script outside the project workspace so ordinary Write, Edit, and MultiEdit operations cannot replace the command target that Claude Code will execute after the edit:

#!/usr/bin/env bash
set -uo pipefail

input="$(cat)"

if ! command -v jq >/dev/null 2>&1; then
  echo "Wrangler config guard skipped: jq is required to parse Claude Code hook input." >&2
  exit 0
fi

if ! command -v node >/dev/null 2>&1; then
  echo "Wrangler config guard skipped: Node.js is required for static config checks." >&2
  exit 0
fi

tool_name="$(printf '%s' "$input" | jq -r '.tool_name // .toolName // empty')"
file_path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.path // .toolInput.file_path // .toolInput.path // empty')"

case "$tool_name" in
  Write|Edit|MultiEdit|write|edit|multiedit) ;;
  *) exit 0 ;;
esac

case "$(basename "$file_path")" in
  wrangler.toml|wrangler.json|wrangler.jsonc) ;;
  *) exit 0 ;;
esac

if [ -z "$file_path" ] || [ ! -f "$file_path" ]; then
  exit 0
fi

if [ -L "$file_path" ]; then
  echo "Wrangler config guard blocked: config path must not be a symlink." >&2
  exit 1
fi

WRANGLER_CONFIG_PATH="$file_path" node <<'NODE'
const fs = require("node:fs");
const pathModule = require("node:path");
const path = process.env.WRANGLER_CONFIG_PATH;
const name = path.split(/[\\/]/).pop();
const maxConfigBytes = 1024 * 1024;
const errors = [];
const warnings = [];

const stats = fs.lstatSync(path);
if (stats.isSymbolicLink()) {
  errors.push("Config path must not be a symlink.");
}

const realPath = fs.realpathSync(path);
const projectRoot = fs.realpathSync(process.cwd());
const relativePath = pathModule.relative(projectRoot, realPath);
if (relativePath.startsWith("..") || pathModule.isAbsolute(relativePath)) {
  errors.push("Config path must stay within the current project.");
}

if (stats.size > maxConfigBytes) {
  errors.push("Config file is too large to inspect safely.");
}

if (errors.length) {
  console.error(`Wrangler config guard: checking ${name}`);
  for (const error of errors) console.error(`[error] ${error}`);
  process.exit(1);
}

const raw = fs.readFileSync(path, "utf8");

function displayName(value) {
  return JSON.stringify(String(value)).replace(/[\u007f-\u009f]/g, (char) => {
    return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
  });
}

function stripJsonc(value) {
  let out = "";
  let inString = false;
  let quote = "";
  let escaped = false;
  let lineComment = false;
  let blockComment = false;

  for (let i = 0; i < value.length; i += 1) {
    const char = value[i];
    const next = value[i + 1];

    if (lineComment) {
      if (char === "\n") {
        lineComment = false;
        out += char;
      }
      continue;
    }

    if (blockComment) {
      if (char === "*" && next === "/") {
        blockComment = false;
        i += 1;
      }
      continue;
    }

    if (inString) {
      out += char;
      if (escaped) {
        escaped = false;
      } else if (char === "\\") {
        escaped = true;
      } else if (char === quote) {
        inString = false;
      }
      continue;
    }

    if (char === "\"" || char === "'") {
      inString = true;
      quote = char;
      out += char;
      continue;
    }

    if (char === "/" && next === "/") {
      lineComment = true;
      i += 1;
      continue;
    }

    if (char === "/" && next === "*") {
      blockComment = true;
      i += 1;
      continue;
    }

    out += char;
  }

  return out.replace(/,\s*([}\]])/g, "$1");
}

function looksSecretLike(key) {
  return /(secret|token|password|private|credential|api[_-]?key|client[_-]?secret)/i.test(key);
}

function checkConfigObject(config) {
  if (!config || typeof config !== "object" || Array.isArray(config)) {
    errors.push("Config did not parse to an object.");
    return;
  }

  if (!config.name) errors.push("Missing top-level `name`.");
  if (!config.compatibility_date) errors.push("Missing top-level `compatibility_date`.");

  if (!config.main && !config.assets && !config.pages_build_output_dir) {
    warnings.push("No `main`, `assets`, or `pages_build_output_dir` found. `main` is optional only for assets-only Workers.");
  }

  if (config.site) {
    warnings.push("`site` is present. Workers Sites is deprecated in Wrangler v4; prefer Workers Static Assets for new projects.");
  }

  if (config.send_metrics !== false) {
    warnings.push("`send_metrics` is not set to false. Review Cloudflare telemetry expectations for this repository.");
  }

  if (config.vars && typeof config.vars === "object") {
    for (const key of Object.keys(config.vars)) {
      if (looksSecretLike(key)) {
        warnings.push(`Top-level vars contains secret-like key ${displayName(key)}; use Wrangler secrets for sensitive values.`);
      }
    }
  }

  const bindingKeys = [
    "vars",
    "kv_namespaces",
    "r2_buckets",
    "d1_databases",
    "queues",
    "durable_objects",
    "services",
    "analytics_engine_datasets",
    "vectorize",
    "ai",
  ];

  if (config.env && typeof config.env === "object") {
    for (const [envName, envConfig] of Object.entries(config.env)) {
      if (!envConfig || typeof envConfig !== "object") continue;

      for (const key of bindingKeys) {
        if (config[key] && !Object.prototype.hasOwnProperty.call(envConfig, key)) {
          warnings.push(`Environment ${displayName(envName)} does not define ${displayName(key)}; Wrangler treats bindings and vars as non-inheritable.`);
        }
      }

      if (envConfig.vars && typeof envConfig.vars === "object") {
        for (const key of Object.keys(envConfig.vars)) {
          if (looksSecretLike(key)) {
            warnings.push(`Environment ${displayName(envName)} vars contains secret-like key ${displayName(key)}; use Wrangler secrets for sensitive values.`);
          }
        }
      }
    }
  }
}

function stripTomlComment(line) {
  let out = "";
  let inString = false;
  let quote = "";
  let escaped = false;

  for (const char of line) {
    if (inString) {
      out += char;
      if (escaped) {
        escaped = false;
      } else if (char === "\\") {
        escaped = true;
      } else if (char === quote) {
        inString = false;
      }
      continue;
    }

    if (char === "\"" || char === "'") {
      inString = true;
      quote = char;
      out += char;
      continue;
    }

    if (char === "#") break;
    out += char;
  }

  return out.trim();
}

function checkToml() {
  const top = new Set();
  const sections = new Set();
  const envSections = new Set();
  const envKeys = new Map();
  const bindingKeys = [
    "vars",
    "kv_namespaces",
    "r2_buckets",
    "d1_databases",
    "queues",
    "durable_objects",
    "services",
    "analytics_engine_datasets",
    "vectorize",
    "ai",
  ];
  let section = "";

  for (const originalLine of raw.split(/\r?\n/)) {
    const line = stripTomlComment(originalLine);
    if (!line) continue;

    const sectionMatch = line.match(/^\s*\[+\s*([A-Za-z0-9_.-]+)\s*\]+/);
    if (sectionMatch) {
      section = sectionMatch[1];
      sections.add(section);
      const envMatch = section.match(/^env\.([A-Za-z0-9_-]+)(?:\.([A-Za-z0-9_.-]+))?/);
      if (envMatch) {
        envSections.add(envMatch[1]);
        if (!envKeys.has(envMatch[1])) envKeys.set(envMatch[1], new Set());
        if (envMatch[2]) envKeys.get(envMatch[1]).add(envMatch[2].split(".")[0]);
      }
      continue;
    }

    const keyMatch = line.match(/^([A-Za-z0-9_$-]+)\s*=/);
    if (!keyMatch) continue;

    const key = keyMatch[1];
    if (!section) {
      top.add(key);
    } else if (section === "vars" && looksSecretLike(key)) {
      warnings.push(`Top-level vars contains secret-like key ${displayName(key)}; use Wrangler secrets for sensitive values.`);
    } else {
      const envVarMatch = section.match(/^env\.([A-Za-z0-9_-]+)\.vars$/);
      if (envVarMatch && looksSecretLike(key)) {
        warnings.push(`Environment ${displayName(envVarMatch[1])} vars contains secret-like key ${displayName(key)}; use Wrangler secrets for sensitive values.`);
      }
    }
  }

  if (!top.has("name")) errors.push("Missing top-level `name`.");
  if (!top.has("compatibility_date")) errors.push("Missing top-level `compatibility_date`.");
  if (!top.has("main") && !sections.has("assets") && !top.has("pages_build_output_dir")) {
    warnings.push("No `main`, `[assets]`, or `pages_build_output_dir` found. `main` is optional only for assets-only Workers.");
  }
  if (sections.has("site")) {
    warnings.push("`site` is present. Workers Sites is deprecated in Wrangler v4; prefer Workers Static Assets for new projects.");
  }
  if (!top.has("send_metrics")) {
    warnings.push("`send_metrics` is not set. Review Cloudflare telemetry expectations for this repository.");
  }

  for (const envName of envSections) {
    const keys = envKeys.get(envName) || new Set();
    for (const key of bindingKeys) {
      if (sections.has(key) && !keys.has(key)) {
        warnings.push(`Environment ${displayName(envName)} does not define ${displayName(key)}; Wrangler treats bindings and vars as non-inheritable.`);
      }
    }
  }
}

console.error(`Wrangler config guard: checking ${name}`);

try {
  if (name === "wrangler.json" || name === "wrangler.jsonc") {
    checkConfigObject(JSON.parse(stripJsonc(raw)));
  } else {
    checkToml();
  }
} catch {
  errors.push("Parse failed: review the Wrangler config syntax before continuing.");
}

for (const warning of warnings) console.error(`[warn] ${warning}`);
for (const error of errors) console.error(`[error] ${error}`);

if (!warnings.length && !errors.length) {
  console.error("Wrangler config guard: no issues found.");
}

process.exit(errors.length ? 1 : 0);
NODE

How It Works

  1. Reads Claude Code hook input from stdin.
  2. Ignores non-edit tools.
  3. Runs only for files named wrangler.toml, wrangler.json, or wrangler.jsonc.
  4. Parses JSON and JSONC with a small comment/trailing-comma stripper.
  5. Parses TOML with conservative section/key heuristics.
  6. Reports required-field errors and advisory warnings.
  7. Exits non-zero only when a parse or required-field error is found.

Safety Notes

This hook is intentionally narrower than a Cloudflare deploy-readiness command. It does not authenticate to Cloudflare, list secrets, execute a dry run, run project package scripts, invoke bundlers, write output directories, or contact the network. That makes it suitable as a first-line edit guard before any trusted pre-deploy workflow.

The tradeoff is that it is not a full Wrangler validator. TOML parsing is heuristic, and Cloudflare can add new config keys over time. Treat warnings as review prompts, not a complete deployment decision.

Privacy Notes

Wrangler config files are not always harmless. They can expose worker names, route patterns, account identifiers, resource IDs, environment names, analytics preferences, and non-secret vars. If the hook reports secret-like vars, move those values to Wrangler secrets before sharing logs or PR comments.

Source Notes

  • Cloudflare documents wrangler.toml, wrangler.json, and wrangler.jsonc as supported config formats and recommends wrangler.jsonc for new projects.
  • The Wrangler configuration docs describe the config file as the Worker source of truth and show required fields such as name, main, and compatibility_date.
  • Cloudflare documents bindings and environment variables as non-inheritable, meaning environments should define their own binding values where applicable.
  • The workers-sdk repository is the official Wrangler source repository.

Duplicate Check

This entry is distinct from the existing /deploy-readiness command, which can run Wrangler auth, secret-list, and deploy dry-run checks in a trusted project. This hook is limited to static config-file review after Claude edits wrangler.toml, wrangler.json, or wrangler.jsonc.

Existing Cloudflare skills and guides cover broader Workers development and deployment workflows. This entry is specifically a Claude Code PostToolUse hook for edit-time Wrangler config safety.

Troubleshooting

  • If the hook never runs, confirm the settings file uses PostToolUse with a Write|Edit|MultiEdit matcher.
  • If it prints a jq message, install jq or replace the shell parser with a project-approved hook wrapper.
  • If it prints a Node.js message, install Node.js or move the static checker into a language already required by the project.
  • If TOML warnings are too noisy, keep the static hook as advisory and run the official Wrangler checks in a trusted pre-deploy workflow.

Source citations

Add this badge to your README

Show that Cloudflare Wrangler Config Guard Hook for Claude Code is listed on HeyClaude. Paste this Markdown into your README — it renders the badge and links back to this page.

Listed on HeyClaude
[![Listed on HeyClaude](https://heyclau.de/badge/hooks/cloudflare-wrangler-config-guard-hook.svg)](https://heyclau.de/entry/hooks/cloudflare-wrangler-config-guard-hook)

How it compares

Cloudflare Wrangler Config Guard Hook for Claude Code side by side with its closest alternative on trust, install, platform support, and disclosed safety notes — all from reviewed registry metadata.

1 trust signal differ across this comparison (Submitter).

Field

Read-only Claude Code PostToolUse hook that checks edited Cloudflare Wrangler config files for required Worker fields, environment inheritance traps, secret-like vars, deprecated Workers Sites usage, and source-of-truth drift before Claude continues.

Open dossier

PreToolUse hook that reviews proposed writes to MCP configuration files and blocks inline credential values, credential-bearing URLs, and broad filesystem roots before they are saved.

Open dossier
Next steps
Trust
Review statusReviewedMaintainer reviewedReviewedMaintainer reviewed
Package trustPackage not verifiedPackage not verified
Source provenanceSource-backedSource-backed
SubmitterDiffersoktofeesh1MkDev11
Install riskReview firstReview first
Notes Safety ✓ Privacy ✓ Safety ✓ Privacy ✓
BrandCloudflare logoCloudflare
Categoryhookshooks
SourceSource-backedSource-backed
Authoroktofeesh1MkDev11
Added2026-06-042026-06-05
Platforms
Harness
Source repo
Safety notesThis hook is static and read-only. It reads only non-symlinked Wrangler config files inside the current project and does not run `wrangler deploy`, `wrangler dev`, `wrangler whoami`, `wrangler secret list`, package scripts, build plugins, or network commands. The hook exits non-zero only for parse failures or missing required top-level Worker fields such as `name` and `compatibility_date`; warnings remain advisory so the user can review them. The TOML checks are heuristic and do not replace Wrangler's own parser or a trusted pre-deploy dry run. Use them as a fast edit-time guard, then run official Wrangler validation in a trusted repository. Do not expand this hook into a dry-run deploy hook unless the team has reviewed local build-script execution, inherited environment variables, and output-directory cleanup. Keep matchers scoped to config file edits. Running config checks after every tool call adds noise without improving Cloudflare safety.Runs before Write, Edit, and MultiEdit tool calls and reads only the pending file path plus proposed new text, including replacement fragments for partial edits. Blocks matching MCP configuration edits with exit code 2 when it finds inline credential values, credential-bearing URLs, or broad filesystem roots for filesystem MCP servers. Does not start MCP servers, contact remote MCP endpoints, edit files, delete files, or inspect existing config beyond the proposed tool input. Text and JSON heuristics can miss unusual config shapes or flag reviewed local setups; use `MCP_CONFIG_PRIVACY_ALLOWLIST` only for documented exceptions. Set `MCP_CONFIG_PRIVACY_MODE=advisory` to warn without blocking while a team tunes its MCP policy.
Privacy notesThe hook reads `wrangler.toml`, `wrangler.json`, or `wrangler.jsonc` only inside the current project, with symlinks rejected and a 1 MiB size cap. Wrangler config files can contain worker names, route patterns, account identifiers, resource IDs, binding names, environment names, analytics settings, and non-secret vars. Hook output may include file paths, config key names, environment names, and secret-like variable names, so avoid pasting terminal logs into public issue comments without review. If a Wrangler config currently contains real secrets in `vars`, the hook can reveal the key names and the surrounding configuration context. Move sensitive values to Wrangler secrets before sharing output.Runs locally and makes no network calls. Does not print credential values, URLs, or full config content; it reports only finding categories. The target file path, finding category, allowlist pattern, and mode variable can still appear in terminal output, Claude Code transcripts, CI logs, or screenshots. Use environment-variable expansion or a secret manager for MCP credentials so private tokens are not committed to project-scoped config files.
Prerequisites
  • Claude Code project where hooks are allowed by user or project policy.
  • Cloudflare Workers or Pages project with `wrangler.toml`, `wrangler.json`, or `wrangler.jsonc` checked into the repository.
  • `jq` available locally to parse Claude Code hook input.
  • Node.js available locally for static JSON/JSONC/TOML heuristics.
  • Claude Code CLI with hooks enabled.
  • bash, jq, grep, sort, and a reviewed `.claude/settings.json` or user-level hook configuration.
  • A team MCP policy for approved servers, credential storage, filesystem scopes, and remote transports.
Install
mkdir -p $HOME/.claude/hooks && touch $HOME/.claude/hooks/wrangler-config-guard.sh && chmod +x $HOME/.claude/hooks/wrangler-config-guard.sh
mkdir -p "$HOME/.claude/hooks" && touch "$HOME/.claude/hooks/mcp-config-privacy-scanner.sh" && chmod +x "$HOME/.claude/hooks/mcp-config-privacy-scanner.sh"
Config
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/wrangler-config-guard.sh"
          }
        ]
      }
    ]
  }
}
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/mcp-config-privacy-scanner.sh"
          }
        ]
      }
    ]
  }
}
Citations
ClaimUnclaimedUnclaimed
Open in the interactive comparison tool

Related guides

Signals

Loading live community signals…

More like this, weekly

A short, calm digest of reviewed Claude resources. Unsubscribe any time.