Add a feature toggle
A toggle turns one capability on for a cohort and can be killed fleet-wide without a release. You will add a Linear MCP server for 25% of ring1-canary, watch validation catch a missing expiry and a forbidden value, and look at the fragment the release records. Commands were run against a copy of examples/acme-corp.
Prerequisites: a built halo (Your first 10 minutes). Work in a copy: cp -r examples/acme-corp policy && cd policy.
1. Write the toggle
Section titled “1. Write the toggle”Create toggles/linear-mcp.yaml:
apiVersion: halos.dev/v1kind: Togglename: linear-mcpdescription: Linear MCP server for a quarter of the canary ringowner: ai-platformdefault: falseaxis: clientrules: - name: ring1-quarter rings: [ring1-canary] percent: 25client: harnesses: claude-code: mcpServers: - name: linear url: https://mcp.linear.app/mcp headers: Authorization: Bearer ${LINEAR_MCP_TOKEN} gemini-cli: mcpServers: - name: linear url: https://mcp.linear.app/mcp${LINEAR_MCP_TOKEN} is an env reference resolved on the device; a literal secret fails the build.
2. Validate
Section titled “2. Validate”$ halo validatewarning toggles[linear-mcp].expires no expiry date; toggles are temporary, set expires (YYYY-MM-DD)OK: policy valid (1 warnings)Add expires: "2026-12-31" under owner, then:
$ halo validateOK: policy valid (0 warnings)$ halo toggle listformat-hook client default=false rules=1 owner=ai-platform expires=2026-11-30github-mcp client default=false rules=1 owner=ai-platform expires=2026-12-31linear-mcp client default=false rules=1 owner=ai-platform expires=2026-12-31sonnet-next-route traffic default=false rules=1 owner=ai-platform expires=2026-12-153. See who gets it, and why
Section titled “3. See who gets it, and why”dev26 and dev45 are both in ring1-canary; the 25% hash picks one:
$ halo toggle eval --user dev26@acme.com | grep -A1 linearoff linear-mcp no rule matched; default off rule ring1-quarter: ring ok ("ring1-canary" in [ring1-canary]), percent no (bucket 4542 < 2500 of 10000)$ halo toggle eval --user dev45@acme.com | grep -A1 linearon linear-mcp rule ring1-quarter matched (on) rule ring1-quarter: ring ok ("ring1-canary" in [ring1-canary]), percent ok (bucket 585 < 2500 of 10000)$ halo toggle eval --user dev45@acme.com --killed linear-mcp | grep -A2 linearoff linear-mcp killed via the signed kill list rule ring1-quarter: ring ok ("ring1-canary" in [ring1-canary]), percent ok (bucket 585 < 2500 of 10000) kill list: killedThe bucket comes from internal/assign with a per-toggle salt, so it is independent of ring and experiment assignment. --killed previews a kill without touching a server.
4. See what the release records
Section titled “4. See what the release records”$ halo release build --ring ring1-canary --release-version 0.1.0 --no-artifacts -o rel.tarwarning: --no-artifacts: halod will not install CLIs from this release unless allowShellInstall is setbuilt rel.tar (sha256:1067b5d5…)$ tar -xOf rel.tar manifest.json | python3 -c 'import sys,jsonm=json.load(sys.stdin); t=[t for t in m["toggles"] if t["name"]=="linear-mcp"][0]print(t["rules"]); print({h:v["linux"][0]["path"] for h,v in t["fragments"].items()})'[{'name': 'ring1-quarter', 'rings': ['ring1-canary'], 'percent': 25}]{'claude-code': '/etc/claude-code/managed-mcp.json', 'gemini-cli': '/etc/gemini-cli/settings.json'}The signed manifest holds the rules and one content-addressed fragment per harness and OS. The release’s ordinary files are the toggle-off state; halod layers the fragment on top only for users the rules turn on. Warnings live in the same manifest, and format-hook shows what an ineffective fragment looks like: toggle format-hook: codex/darwin: fragment changes nothing (already in the profile, or unsupported by the adapter).
5. Try a forbidden value
Section titled “5. Try a forbidden value”Toggle fragments go through the same guardrails as a profile. Add an env block to claude-code that sets ANTHROPIC_BASE_URL:
$ halo validateerror toggles[bad].client.harnesses.claude-code.env.ANTHROPIC_BASE_URL env prefix ANTHROPIC_ is reserved for the renderer1 errors, 0 warnings$ echo $?2halo release build refuses the same policy (error: policy has validation errors). Remove the file to continue.
What just happened
Section titled “What just happened”validateenforced the toggle contract: anowner, anexpiresdate (a warning when missing), and the profile guardrails on every fragment.toggle evalre-ran the targeting rules exactly as the gateway andhaloddo, with the hash bucket in the trace.- The release recorded rules and fragments in its signed manifest, so neither can change in transit. Nothing is on or off for anyone until you publish a release and
halodpulls it. - Turning it off later takes a kill, not a release: Kill a bad change in 10 seconds.
- Feature toggles for rule semantics and the traffic axis
halo toggle stalelists toggles pastexpires; delete them when the rollout is done- Canary a new model uses a traffic toggle