---
name: unleash-flag
description: This skill should be used when the user asks to "add a feature flag", "gate a feature", "put this behind a flag", "add a workspace flag", "feature flag this", "make this configurable per workspace", "configure Unleash", or references "flags.json", "useWorkspaceFlag", "useFlag", "SyncFlags", or "controlled rollout" of a feature.
---

# Unleash Feature Flag

Workspace-scoped feature flags for the Ambient Code Platform — naming, gating, evaluation, and lifecycle management.

## Checklist

Every new flag requires these steps. Skip none.

1. **Define the flag** in `components/manifests/base/core/flags.json`
2. **Gate the frontend** using the correct hook for scope
3. **Update tests** to mock the flag hook
4. **Verify** type-check and tests pass

## 1. Define the Flag

Add to `components/manifests/base/core/flags.json`:

```json
{
  "name": "category.feature-name.enabled",
  "description": "Human-readable purpose of this flag",
  "tags": [
    {
      "type": "scope",
      "value": "workspace"
    }
  ]
}
```

### Naming Convention

**All new flags** must use the `category.feature-name.enabled` format:

```
category.feature-name.enabled
```

| Segment | Rules | Examples |
|---------|-------|---------|
| `category` | One of the standard categories below | `runner`, `model`, `integration` |
| `feature-name` | Lowercase, hyphen-separated, descriptive | `gemini-cli`, `coderabbit` |
| `.enabled` | Required suffix for all new flags | — |

**Standard categories:**

| Category | When | Example |
|----------|------|---------|
| `integration` | External service integrations | `integration.coderabbit.enabled` |
| `runner` | Runner type availability | `runner.gemini-cli.enabled` |
| `model` | Model availability (auto-generated by SyncFlags) | `model.claude-sonnet-4-5.enabled` |
| `feature` | Platform capabilities | `feature.scheduled-export.enabled` |

**Grandfathered flags** (do not rename):
- `jira-write`
- `ldap.autocomplete.enabled`
- `scheduled-session.reuse.enabled`

These predate the convention. New flags must not follow their patterns.

**Rules:**
- **Unique across the entire system** — never reuse a retired flag name
- **Descriptive** — someone unfamiliar should understand the scope from the name alone
- **Dot-delimited** with category prefix — enables grouping in workspace settings UI
- **Lowercase with hyphens** within segments (not underscores, not camelCase)

### Workspace Settings Visibility

The `scope:workspace` tag is what makes a flag appear in the workspace settings UI. Without it, the flag exists in Unleash but workspace admins cannot toggle it.

Backend `SyncFlags` runs at startup: reads `flags.json`, creates flags in Unleash (type: `release`, disabled by default, 0% rollout), and tags them. Flags without `EnabledByDefault: true` start **off**.

To force a re-sync, restart the backend. To clear a stuck flag, archive + purge it in Unleash admin before restarting.

## 2. Gate the Frontend

### Choosing the Right Hook

**Default: `useWorkspaceFlag`** — always prefer this hook. It respects ConfigMap overrides set by workspace admins.

```tsx
import { useWorkspaceFlag } from "@/services/queries/use-feature-flags-admin";
const { enabled } = useWorkspaceFlag(projectName, "category.feature.enabled");
```

**Exception: `useFlag`** — only for the rare case where no project context is available (e.g., cluster-level admin pages like `/integrations`). It bypasses workspace overrides entirely and returns only the Unleash global default. Do not use it in project-scoped pages.

```tsx
import { useFlag } from "@/lib/feature-flags";
const enabled = useFlag("category.feature.enabled");
```

| Hook | Scope | Evaluation |
|------|-------|------------|
| `useWorkspaceFlag` | Project-scoped pages (default) | ConfigMap override > Unleash default |
| `useFlag` | Cluster-scoped pages only (exception) | Unleash default only |

### Gating Patterns

**Conditionally render a component:**
```tsx
const { enabled: featureEnabled } = useWorkspaceFlag(projectName, "category.feature.enabled");

// In JSX:
{featureEnabled && <FeatureComponent />}
```

**Conditionally include in an array:**
```tsx
const items = [
  { key: "always-visible", name: "Always" },
  ...(featureEnabled
    ? [{ key: "gated", name: "Gated Feature" }]
    : []),
];
```

**Extract project name from route params** (when not passed as prop):
```tsx
import { useParams } from "next/navigation";
const params = useParams();
const projectName = params.name as string;
```

### Backend Gating (when needed)

For full-stack gating (not always necessary — frontend-only is often sufficient):

```go
// Global check
if !FeatureEnabled("category.feature.enabled") {
    c.JSON(http.StatusForbidden, gin.H{"error": "Feature not enabled"})
    return
}

// Request-scoped (includes user/IP context for Unleash strategies)
if !FeatureEnabledForRequest(c, "category.feature.enabled") {
    c.JSON(http.StatusForbidden, gin.H{"error": "Feature not enabled"})
    return
}
```

### Backend Middleware Gating

For endpoints that should be entirely hidden behind a flag, create a middleware in `routes.go` that checks `FeatureEnabledForRequest()` and returns 404 when the flag is off:

```go
// routes.go — gate an entire route group
flagged := router.Group("/api/v1/feature")
flagged.Use(func(c *gin.Context) {
    if !handlers.FeatureEnabledForRequest(c, "category.feature.enabled") {
        c.AbortWithStatus(http.StatusNotFound)
        return
    }
    c.Next()
})
{
    flagged.GET("/resource", handlers.ListResource)
    flagged.POST("/resource", handlers.CreateResource)
}
```

This returns 404 (not 403) when the flag is off, so the endpoint appears not to exist. Use this for features that shouldn't even be discoverable until enabled.

### E2E Testing with Feature Flags

When writing E2E tests for flagged features, the Unleash admin token must be set as an environment variable. **Never paste or export the token inline in a shell session** — this risks exposing it in shell history, logs, or terminal recordings.

Obtain the token from Unleash UI > API Access or from deployment secrets, then set it using a non-echoing prompt or a secret manager:
```bash
# Non-echoing prompt (token not stored in shell history)
read -rs CYPRESS_UNLEASH_ADMIN_TOKEN && export CYPRESS_UNLEASH_ADMIN_TOKEN
```

In CI, inject it via a secret reference (e.g. GitHub Actions secret, Vault, or sealed secret) — never hard-code or `echo` the value.

> **Note**: When logging token-related errors, use `len(token)` to confirm presence without exposing the value. Never include the full token in error messages, API responses, or commit history.

Map it in `e2e/cypress.config.ts`:
```typescript
env: { UNLEASH_ADMIN_TOKEN: process.env.CYPRESS_UNLEASH_ADMIN_TOKEN }
```

```typescript
// e2e/cypress/e2e/flagged-feature.cy.ts
describe('Flagged Feature', () => {
  before(() => {
    // Enable the flag via Unleash admin API
    cy.request({
      method: 'POST',
      url: 'http://localhost:4242/api/admin/projects/default/features/category.feature.enabled/environments/development/on',
      headers: { Authorization: Cypress.env('UNLEASH_ADMIN_TOKEN') },
    }).its('status').should('eq', 200);
  });

  after(() => {
    // Disable the flag after tests
    cy.request({
      method: 'POST',
      url: 'http://localhost:4242/api/admin/projects/default/features/category.feature.enabled/environments/development/off',
      headers: { Authorization: Cypress.env('UNLEASH_ADMIN_TOKEN') },
    }).its('status').should('eq', 200);
  });

  it('renders when flag is enabled', () => {
    // ... test the feature
  });
});
```

## 3. Update Tests

Any component that now calls `useWorkspaceFlag` or `useFlag` needs its tests updated.

**Mock `useWorkspaceFlag`:**
```tsx
vi.mock("@/services/queries/use-feature-flags-admin", () => ({
  useWorkspaceFlag: () => ({ enabled: true }),
}));
```

**Mock `useParams`** (if newly added):
```tsx
vi.mock("next/navigation", () => ({
  useParams: () => ({ name: "test-project" }),
}));
```

**Mock `useFlag`:**
```tsx
vi.mock("@/lib/feature-flags", () => ({
  useFlag: () => true,
}));
```

Test both states: confirm the gated element renders when enabled, and is absent when disabled.

## 4. Verify

```bash
cd components/frontend && npx tsc --noEmit    # Type-check
cd components/frontend && npx vitest run       # Unit tests
```

Both must pass with zero errors.

## Evaluation Order

When a flag is evaluated at runtime, the platform checks in priority order:

1. **ConfigMap override** (`feature-flag-overrides` in workspace namespace) — set by workspace admin via settings UI
2. **Unleash default** — global state from Unleash server
3. **Code default** — `false` (fail-closed)

This means a workspace admin can override the global Unleash state in either direction.

## Lifecycle

| Phase | Action |
|-------|--------|
| **Create** | Add to `flags.json`, gate frontend, assign ownership |
| **Rollout** | Workspace admins enable per-workspace via settings UI |
| **GA** | Remove flag checks from code, remove from `flags.json`, create Jira for cleanup tracking |
| **Cleanup** | Archive flag in Unleash, remove stale ConfigMap overrides |

Treat flags as technical debt. When a feature is fully rolled out, remove the flag — don't leave it permanently enabled.

**When a feature reaches GA**, create a Jira issue (use `/jira-log`) to track cleanup in this order:
1. Verify feature is fully rolled out (all workspaces enabled or no longer need it)
2. Remove `useWorkspaceFlag` / `useFlag` calls from code and deploy
3. Remove the flag from `flags.json` and deploy (SyncFlags stops recreating it)
4. Remove any ConfigMap overrides in workspace namespaces
5. Archive and purge the flag in Unleash

**Exceptions for long-lived flags:** kill switches for graceful degradation, and debug flags for expensive tracing.

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Using `useFlag()` in project-scoped page | Use `useWorkspaceFlag(projectName, flagName)` |
| Missing `scope:workspace` tag | Flag won't appear in workspace settings UI |
| Forgetting to mock hook in tests | Tests crash with "Cannot read properties of null" |
| Reusing a retired flag name | Unleash conflict error on sync — archive + purge first |
| Checking flag in multiple places | Evaluate once, pass result down |
| No test for disabled state | Add test verifying gated element is hidden when flag is off |
