Flags and experiments belong in the repo.

One .md file per feature flag or A/B test, in the repo with the code. Holdouts and staged rollouts are the same format, versioned in git and reviewable in a PR. Your coding agent reads dif/context.json on session start, so prior learning travels with the work.

Get started
or install now, no signup
$ npm install -g @dif.sh/cli
---
id: checkout-cta-v2
status: active
owner: ada@acme.dev
surface: checkout
hypothesis: >
A more urgent CTA copy on the checkout button will lift
completed-checkout rate among returning visitors.
audience:
include:
- returning_visitor: true
exclude:
- country: [US-CA] # legal hold
variants:
- id: control
weight: 50
- id: variant_a
weight: 50
summary: "Get it today" copy
metrics:
primary: completed_checkout
guardrails: [refund_rate, support_ticket_rate]
exclusion_group: checkout-copy
created: 2026-05-14
---
 
## Brief
 
Returning visitors on checkout show a 14% drop-off between
cart review and payment confirmation. Hypothesis: "Buy now"
reads as transactional where "Get it today" reads as
immediate-benefit. Copy only. No layout or price changes.
 
## Rationale
 
Surface notes flag prior copy tests at low traffic were
inconclusive. Running 50/50 with a two-week minimum to
clear the variance floor.
 
## Decision
 
<!-- drafted by `dif conclude` -->

More than a place to keep flags.

Segmentation, conflict checks, analytics, and cleanup are part of the file format and the CLI, not a separate service you log into.

dif/context.json

Your agent reads the experiments.

Every dif build regenerates dif/context.json: active flags and experiments, variants, recent learnings. Coding agents pick it up on session start, like a CLAUDE.md for experiments.

dif/config.yaml

Target on attributes you declare.

Declare audience attributes once, then include or exclude on country, plan, or returning_visitor. Values arrive at runtime; no customer list is ever committed.

dif.track()

Send results where you already look.

Add a Dif Cloud key and dif.track() computes lift for you, no join code. Or run dif init --events custom and own the two handlers that forward events to Segment, Amplitude, or your warehouse.

dif build

Clashes fail the build.

The build resolves the exclusion graph and refuses to compile when two live tests would collide on one user. It breaks in CI, not in production.

exclusion_group:

Keep tests off each other.

One frontmatter line groups experiments so no user is bucketed into two at once. Enforced at build, every time.

dif conclude

Conclude, don't abandon.

Archives the file to dif/experiments/concluded/, drafts the Decision block, and appends one line to the surface log so the next test starts informed.

Up and running in one build.

Install, point your code at a flag or experiment id, and ship. Same files, same CLI, whichever way you render.

Install
$ npm install -g @dif.sh/cli
$ npm install @dif.sh/sdk
Build
$ dif init
$ dif new checkout-cta --surface checkout
$ dif build
# emits dif/generated/client.ts + dif/context.json
Use it· checkout.ts
// import the generated client once, at boot
import "dif/generated/client";
import { dif } from "@dif.sh/sdk";

const checkoutCta = dif("checkout-cta", {
  control: () => "Buy now",
  variant_a: () => "Get it today",
});

button.textContent = checkoutCta();
Read the full docs →

Just files. That's the point.

It all lives in one dif/ folder, checked into your repo. Each flag or experiment is a single .md file: experiments/active/ for what's live, experiments/concluded/ for what's done. surfaces/ keeps the running log of what each screen has taught you, and the agent's context.json sits right alongside.

your-app/
├── dif/
│ ├── experiments/
│ │ ├── active/
│ │ │ ├── checkout-cta-v2.md # live, 7 days in
│ │ │ └── pricing-headline.md
│ │ └── concluded/
│ │ ├── 2026-04-hero-image.md
│ │ └── 2026-03-signup-friction.md
│ ├── surfaces/
│ │ ├── checkout.md # context + log
│ │ ├── pricing.md
│ │ └── signup.md
│ ├── config.yaml
│ ├── context.json # read by the agent
│ └── generated/ # gitignored
└── # ... the rest of your app

A flag, an A/B test, a holdout, and a staged rollout are the same file.

Different frontmatter, same shape. Fewer concepts to learn, fewer ways to write a bug.

Git is the audit log. PR review is the approval flow.

No separate database or change history to reconcile against your code.

Guardrails compile to a tiny client. Production stays small.

Exclusion groups and conflict detection all happen at dif build, avoiding clashes and failed experiments.

The schema is in git. Customer data isn't.

Audiences declare which attributes you target on (country, plan, returning_visitor). Values arrive at runtime from your app's user context. No customer list ever gets committed.

One CLI, start to finish.

Each command is small, scriptable, and friendly to coding agents. None of them open a browser. None of them ask you to log in.

1 Author Set up once, then draft each new test.
$dif init

Sets up the folder.

Creates the dif/ folder, a config file, and one .gitignore line. All of it checked into git.

created dif/experiments/{active,concluded} created dif/surfaces/ wrote dif/config.yaml
$dif new

Drafts a new experiment.

Fills in the frontmatter and reads the surface log, so the new test starts from what you already learned.

→ reading dif/surfaces/checkout.md found 3 prior learnings → drafted dif/experiments/active/checkout-cta-v3.md status: draft, owner: ada@acme.dev
2 Verify Catch problems early. See exactly what a user gets.
$dif validate

Checks everything lines up.

Frontmatter is valid, the owner and surface exist, exclusion groups do not clash, and no variant in code points at a missing test.

12 experiments parsed 4 surfaces resolved exclusion groups: ok 1 missing test: hero-image-v1
$dif qa

Shows what a user gets.

Prints the exact variant a user lands in, and hands you a preview link with that variant forced. No dashboard needed.

trace u_8131: • checkout-cta-v2 → variant_a (bucket 7142) • pricing-headline → value (bucket 71) • signup-headline ↛ audience miss same user_id, same bucket, every time.
3 Compile & archive Build the client into your app. Close tests when they're done.
$dif build

Builds the client into your app.

Generates a typed client your app imports directly, so production never calls out to pick a variant. Refreshes context.json, and fails on any conflict.

→ compiling 12 active... typed client → dif/generated/client.ts context → dif/context.json conflict: two tests share group 'checkout-copy'
$dif conclude

Wraps up a test.

Moves the file to dif/experiments/concluded/, drafts the Decision block, and adds one line to the surface log so the next test starts where this one ended.

→ moved dif/experiments/concluded/2026-05-checkout-cta-v2.md → drafted ## Decision block → appended to dif/surfaces/checkout.md "+2.1% completed-checkout on returning. Shipped."
Full CLI reference →

Yesterday's learning is in tomorrow's draft.

Every experiment runs on a surface. Each surface has its own markdown file with a description, the known landmines, and a running log of what you have learned. When your next test gets drafted on that surface, the surface file is the first thing the PR mentions or the agent reads.

One line per concluded test, written by the agent at the moment of conclusion. Over a year, you build a real institutional memory of what works on a given screen and what does not.

surfaces/checkout.md ## Learnings
2026-05-28 checkout-cta-v2: "Get it today" lifted completed-checkout by 2.1% (CI 0.6–3.5%). Shipped. Returning only.
2026-04-11 trust-badges-row: no effect on conversion. Mild positive on support tickets (-4%).
2026-03-02 express-checkout-default: -1.8% on AOV. Reverted. Do not re-run without basket-size guardrail.
2026-02-19 apple-pay-prominence: +0.9% on mobile, flat on desktop. Shipped to mobile only.
▼ read on next "dif new" ▼
experiments/active/checkout-cta-v3.md draft, just now
---
id: checkout-cta-v3
status: draft
surface: checkout
hypothesis: >
  v2 won on returning visitors. Test whether
  the same copy lifts cold traffic, where
  prior copy tests were inconclusive.
---

## Brief

Surface log shows v2 shipped at +2.1% on
returning, cold unchanged. This is a follow-on
to close that gap. ← drafted from log

When your team wants the view across every repo.

Dif Cloud is the optional hosted layer. It reads your repo and gives the team a live read on every experiment, with the confidence intervals to make the call. Git stays canonical: every change it proposes lands as a PR you review.

Dif Cloud dashboard showing live experiments across surfaces
  • Live pulse

    Every running experiment, its lift, and a ready-to-conclude flag, on one page.

  • Confidence intervals

    Lift over time with a 95% interval, control and variant compared side by side.

  • Verdict as a PR

    Cloud drafts the ship, hold, or investigate call into the experiment's .md.

Put your next flag in git.

$ npm install -g @dif.sh/cli

Already shipping? Open Dif Cloud →