diff --git a/README.md b/README.md
index 15448d5..fb5f939 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,7 @@ import { rep } from '@rep-protocol/sdk';
// PUBLIC vars — synchronous, no async, no loading state
const apiUrl = rep.get('API_URL');
-const flags = rep.get('FEATURE_FLAGS');
+const flags = rep.get('FEATURE_FLAGS'); // see rep-protocol.dev/guides/feature-flags/
// SENSITIVE vars — encrypted, decrypted on demand
const key = await rep.getSecure('ANALYTICS_KEY');
diff --git a/docs/src/content/docs/agents.mdx b/docs/src/content/docs/agents.mdx
index 3b464df..0b7f27c 100644
--- a/docs/src/content/docs/agents.mdx
+++ b/docs/src/content/docs/agents.mdx
@@ -415,6 +415,7 @@ When this page is not enough, these are the pages to read next — each is avail
- [Quick Start](/quick-start.md) — the five-minute path
- [How REP Works](/concepts/how-it-works.md) — startup sequence and injection mechanics
- [Variable Classification](/concepts/variable-classification.md) — tier rules and guardrail internals
+- [Feature Flags](/guides/feature-flags.md) — one `csv` variable, flipped without a rebuild
- [Security Model](/concepts/security-model.md) — threat analyses and hardening
- [Wire Format](/concepts/wire-format.md) — payload JSON, encrypted blob layout, HMAC
- [SDK API](/reference/sdk.md) — full client reference
diff --git a/docs/src/content/docs/concepts/variable-classification.mdx b/docs/src/content/docs/concepts/variable-classification.mdx
index 75a7d8d..5824ab3 100644
--- a/docs/src/content/docs/concepts/variable-classification.mdx
+++ b/docs/src/content/docs/concepts/variable-classification.mdx
@@ -70,6 +70,8 @@ At startup, the gateway scans all `REP_PUBLIC_*` values for patterns indicating
| **Known key formats** | AWS keys (`AKIA...`), JWTs (`eyJ...`), GitHub tokens (`ghp_...`), Stripe keys (`sk_live_...`), private keys (`-----BEGIN`) |
| **Length anomalies** | Strings > 64 characters that may be encoded secrets |
+A variable declared `type: csv` in the [manifest](/guides/manifest/) is scanned one element at a time (split on commas, whitespace trimmed) rather than as one string. A long list of short feature flags therefore doesn't trip the length check under `--strict`. A long or secret-shaped token inside the list is still flagged, and the warning names its position (`csv element 3: …` in the message, `csv_element` in the log). Every other value is scanned whole, and so is every value in the dev plugins, which [don't read the manifest](/guides/feature-flags/#dev-plugins).
+
When a potential misclassification is detected:
- The gateway logs a **WARNING** with the variable name (never the value)
diff --git a/docs/src/content/docs/guides/development.mdx b/docs/src/content/docs/guides/development.mdx
index 9789612..76b0322 100644
--- a/docs/src/content/docs/guides/development.mdx
+++ b/docs/src/content/docs/guides/development.mdx
@@ -69,7 +69,7 @@ The easiest way to get full REP support in development. The plugin injects the s
## Option B: Default values (simplest)
diff --git a/docs/src/content/docs/guides/feature-flags.mdx b/docs/src/content/docs/guides/feature-flags.mdx
new file mode 100644
index 0000000..4b85eea
--- /dev/null
+++ b/docs/src/content/docs/guides/feature-flags.mdx
@@ -0,0 +1,88 @@
+---
+title: Feature Flags — One csv Variable, Flipped Without a Rebuild
+description: Use a single REP_PUBLIC_FEATURE_FLAGS csv variable for deployment-wide feature flags. Manifest declaration, SDK reads, guardrail behaviour, flipping flags without a rebuild, and what REP flags are not.
+---
+
+import { Aside } from '@astrojs/starlight/components';
+
+REP can carry simple feature flags: switches that are on or off for a whole deployment. Use one `csv` variable that lists the flags that are on.
+
+## Declare one variable
+
+```yaml
+# .rep.yaml
+variables:
+ FEATURE_FLAGS:
+ tier: public
+ type: csv
+ default: ""
+ pattern: "([a-z0-9-]+(,[a-z0-9-]+)*)?" # optional: kebab-case names, no spaces
+ description: "Flags that are on, comma-separated"
+```
+
+Use one variable for all flags, not one per flag. Adding a flag is then a value change, not a manifest change.
+
+With `default: ""`, an unset `REP_PUBLIC_FEATURE_FLAGS` becomes an empty list, so every flag is off (see [Defaults](/guides/manifest/#defaults)). The optional `pattern` rejects typos such as stray spaces or capitals at startup.
+
+## Read it with the SDK
+
+```typescript
+import { rep } from '@rep-protocol/sdk';
+
+const enabled = new Set(
+ rep.get('FEATURE_FLAGS', '').split(',').map((f) => f.trim()).filter(Boolean),
+);
+
+export const isOn = (flag: string) => enabled.has(flag);
+```
+
+A flag is **off unless it is named**. There is no list of known flags to keep in sync and no "false" value to set.
+
+The second argument to `rep.get()` covers the case with no gateway at all, such as unit tests or a plain `vite dev`. Behind the gateway, the manifest default already guarantees a string.
+
+For example, a single image might serve a marketing landing page at `/` on its hosted deployment and redirect `/` straight to sign-in everywhere else:
+
+```bash
+# hosted deployment
+REP_PUBLIC_FEATURE_FLAGS=landing-page
+# self-hosted deployment: leave it unset — every flag is off
+```
+
+```typescript
+if (location.pathname === '/' && !isOn('landing-page')) location.replace('/login');
+```
+
+## Flags are public
+
+`FEATURE_FLAGS` is a PUBLIC variable, so its value is in the page source of every page the gateway serves.
+
+- **Never put a secret in a flag name.** That includes a token, a customer name, or an unreleased product's codename you can't disclose.
+- **A flag is not access control.** Hiding a button does not stop anyone calling the API behind it, so the server must still check permissions.
+
+## Guardrails judge each flag, not the list
+
+The gateway's [guardrails](/concepts/variable-classification/#automatic-secret-detection-guardrails) check a `csv` value one element at a time. A long list of short flags can't make `--strict` refuse to start, and an element that looks like a secret is still flagged.
+
+### Dev plugins
+
+
+
+## Flipping a flag
+
+You flip a flag by changing the environment, not the image:
+
+- **Restart:** set `REP_PUBLIC_FEATURE_FLAGS` and restart the container or roll the deployment.
+- **Hot reload:** with `--hot-reload` and `--env-file`, the gateway picks up edits to that file without a restart; it does not see changes to its own process environment. Pages using `rep.onChange('FEATURE_FLAGS', …)` or a framework adapter update in place. See [Hot Reload](/concepts/hot-reload/), the [Kubernetes recipe](/deployment/kubernetes/), and the end-to-end [Next.js example](/examples/nextjs-csr-embedded/).
+
+## What this is not
+
+A REP flag has one value per deployment, shared by every visitor. That makes it a good fit for environment differences, kill switches and staged launches. It cannot do:
+
+- per-user or per-account targeting;
+- percentage or gradual rollouts;
+- A/B tests or experiments;
+- an audit trail or a UI for non-engineers.
+
+If you need any of those, use a dedicated feature-flag service. REP can still deliver that service's public client key as a PUBLIC variable.
diff --git a/docs/src/content/docs/guides/manifest.mdx b/docs/src/content/docs/guides/manifest.mdx
index d71ce01..31163a3 100644
--- a/docs/src/content/docs/guides/manifest.mdx
+++ b/docs/src/content/docs/guides/manifest.mdx
@@ -91,7 +91,7 @@ settings:
| `url` | Must be a valid URL |
| `number` | Must parse as a number |
| `boolean` | Must be `true`, `false`, `1`, or `0` |
-| `csv` | Comma-separated values |
+| `csv` | Comma-separated values. [Guardrails](/concepts/variable-classification/#automatic-secret-detection-guardrails) scan each element separately. See [Feature Flags](/guides/feature-flags/) |
| `json` | Must be valid JSON |
| `enum` | Must match one of the `values` array entries |
diff --git a/docs/src/content/docs/reference/plugins/next.mdx b/docs/src/content/docs/reference/plugins/next.mdx
index 16da353..bd5d3d2 100644
--- a/docs/src/content/docs/reference/plugins/next.mdx
+++ b/docs/src/content/docs/reference/plugins/next.mdx
@@ -65,6 +65,7 @@ Only needed if you read `REP_SENSITIVE_*` variables with `rep.getSecure()` / `us
- **Server Component only.** `RepScript` reads the filesystem (`env`) and generates keys server-side; it cannot be used in a Client Component. Consume the injected payload from Client Components via `rep.get()` / `useRep()` as usual — the SDK reads the DOM, not React context.
- **Script content is Go-escaped** (`<`, `>`, `&` → `<`, `>`, `&`) before being written via `dangerouslySetInnerHTML`, preventing `` breakout from untrusted env values. The tag still carries `type="application/json"`, so the browser never executes it.
- **Ephemeral keys are process-scoped**, held in a module-level singleton (`getOrCreateKeys()`) so the same key survives across route handler invocations within one `next dev` process.
+- **Guardrails scan every `REP_PUBLIC_*` value whole**, including `csv` values the gateway scans by element ([details](/guides/feature-flags/#dev-plugins)).
- **Byte-identical payload** to the Go gateway and the Vite plugin (sorted keys, same HMAC + SRI format) — `rep.verify()` and `rep.meta()` behave the same regardless of which one produced the payload.
## App Router only
diff --git a/docs/src/content/docs/reference/plugins/vite.mdx b/docs/src/content/docs/reference/plugins/vite.mdx
index 3617ca2..50e5ca9 100644
--- a/docs/src/content/docs/reference/plugins/vite.mdx
+++ b/docs/src/content/docs/reference/plugins/vite.mdx
@@ -48,7 +48,7 @@ repPlugin({
- **Injects `