Agent skills.
dif init installs four Claude Code skills into .claude/skills/. They teach
a coding agent the dif workflow: reading the surface log, drafting frontmatter, writing a decision
that becomes a learning, and acting on dif.sh Cloud verdicts.
Three skills map onto the daily loop: generate surfaces → author an experiment → conclude it. The fourth, triage, reads dif.sh Cloud verdicts for every active experiment and turns each one
that needs action into a repo change. Each is a plain SKILL.md with a trigger description; an agent loads one when your request matches
(say "draft an experiment" and dif-author-experiment picks it up). They're markdown.
Read them like any file in the repo. dif init --agents-only overwrites an edit to them.
.claude/skills/ dif-author-experiment/ # draft + iterate on an experiment dif-conclude-experiment/ # write the decision, archive, learn dif-generate-surfaces/ # propose the surface set from the codebase dif-triage-experiments/ # act on Cloud verdicts: ramp, turn off, conclude CLAUDE.md # managed dif block AGENTS.md # managed dif block .github/copilot-instructions.md # managed dif block .cursor/rules/dif.mdc # dif's Cursor project rule
All of this is installed by dif init unless you pass --agents none (the deprecated --no-agent-files alias still works too). Scaffold a subset with, say, --agents claude. The skills read from dif/context.json and the surface
logs, so they stay current as your experiments change, with no separate sync step. The triage
skill is new in dif v0.7.0. After you upgrade, run dif init --agents-only to get it.
dif-author-experiment
Drafting, editing, fixing, or iterating on an experiment .md under dif/experiments/active/. It covers the loop from dif new through dif validate to dif build.
Triggers on: create / draft / edit / fix an experiment, or any mention of an A/B
test, variant, audience, hypothesis, surface, exclusion group, or the commands dif new, dif validate, dif build.
Reads context first
The draft is downstream of three reads, so the skill does them before writing anything:
dif/context.json: what's active, on which surface, and how long it's been running.dif/surfaces/<surface>.md: the## Learningslog, so it builds on prior findings instead of re-testing an answered question.dif/config.yaml: the declaredaudience_attributesit's allowed to write predicates over.
The loop
$dif new <kebab-id> --surface <surface> # fill in hypothesis, ## Brief, ## Rationale, audience, metrics $dif validate # collects every error at once $dif build # also runs validate $git add dif/ && git commit
It fills what dif new leaves as stubs, with opinions:
- hypothesis: one falsifiable sentence naming the change, the expected primary-metric direction, and the audience.
- variants: at least two, weights summing to exactly 100,
controlkept as the first id. - audience: predicates only over attributes declared in
dif/config.yaml. - metrics.primary: exactly one metric this experiment is moving.
- exclusion_group: added when
E007fires on a same-surface overlap.
The skill ships reference files for the details: frontmatter.md (full schema + a worked
example), validation-errors.md (every E/W code with its fix),
and audiences.md (predicate grammar and the resolver contract).
dif-conclude-experiment
Concluding an experiment that has run its course: getting the result from dif.sh Cloud, writing a
decision, archiving the file, and running dif conclude. Edits are computed before any
write; a later filesystem failure rolls back on a best-effort basis.
Triggers on: conclude / finish / ship / wrap up an experiment, recording a learning
or writing a decision, or the command dif conclude.
Gets the evidence first
The skill reads the numbers before it writes a word. With the dif MCP server connected, it calls get_experiment_results for the experiment id. Without it, it calls the REST endpoint
with $DIF_TOKEN. With neither, it asks you to connect one or to give it the
numbers. When dif/config.yaml has mode: custom under events:, it asks for the numbers. It never invents them. Setup is in MCP and API.
Concludes on a verdict
The skill concludes only when the Cloud verdict is ship, turn_off or conclude_inconclusive. For any other verdict it stops and
shows the reasons. You can override in chat, and the decision then says so, for example "Concluded early by owner request; not significant."
Starts from the draft
For those three verdicts Cloud writes a one-line decision_draft. The skill keeps its
numbers and adds context only:
$dif conclude checkout-cta-v2 --decision "Shipped variant_a. +2.1% completed_checkout (95% CI +0.8% to +3.4%), p=0.004 over 14d. Guardrails: refund_rate clean."
What you write becomes the newest line in the surface's ## Learnings log, and the next dif new on that surface embeds it into the draft. The skill reads that log in dif/surfaces/<surface>.md as the source of truth, and calls get_learnings only when you ask about other surfaces.
That one command:
- moves
dif/experiments/active/<id>.md→dif/experiments/concluded/<YYYY-MM>-<id>.md; - sets
status: concludedandconcluded: <today>(the command changes only an exactstatus: activeline; for a file withstatus: draftthe skill then setsstatus: concludeditself); - fills the
## Decisionblock; - appends one dated line under
## Learningson the surface.
Agents always pass --decision. Without it, dif conclude opens $EDITOR, and with no terminal it fails with no --decision given and stdin is not a terminal. Add --json to get ok, moved_to, decision_drafted, surface_appended and summary back as JSON.
A good decision is one line: did the hypothesis hold, by how much, over what period, and what's the
next move (ship / kill / iterate). After concluding, the skill runs dif validate and dif build, then removes each dif("<id>", ...) call site that W001 names. It keeps the winning variant's code for a ship, and the control code for a
kill or an inconclusive result. Then it commits dif/ and the call-site change. When
the triage skill sent it, it does not commit, and on a pull request it also leaves app code alone
and lists those call sites in the PR.
"Inconclusive." (no next move), "Looked good, shipping it." (no metric or magnitude), "See Notion doc." (the surface log is the source of truth for future drafts; links rot). Either you have an answer or
you're not ready to conclude.dif-generate-surfaces
Producing the initial dif/surfaces/<name>.md set by reading the app's structure
and product context. Most useful right after dif init (which writes only the one
default surface), or when dif new --surface X exits with "surface does not exist."
Triggers on: set up / scaffold / populate / propose surfaces, especially a sparse dif/surfaces/, or a dif new that failed on a missing surface.
A surface is a unit of experimentation agency, not a unit of code: the place where you'd reasonably A/B test something. A typical app has 3-8 surfaces, not one per route or component. The skill:
- surveys existing surfaces and
dif/context.json, scoping to what's missing; - reads product context (
README.md,package.json, the routing files) to learn where experimentation actually happens; - drafts 3-8 coarse-grained candidates, each with a one-line rationale;
- confirms the list with you before writing anything;
- writes each file in the same format
dif initemits, then runsdif validateanddif build.
# Surface: checkout (One-sentence description: where in the app is this surface? Who sees it?) ## Known landmines (Vendor DOM, regulated regions, race conditions — one bullet each. Empty to start.) ## Learnings (One line per concluded test, appended by dif conclude. Empty to start.)
It keeps names kebab-case and singular (checkout, search-results), leaves ## Known landmines and ## Learnings empty (both accrue from real
experience; guesses pollute future drafts), and never auto-flips default_surface in
your config. From here, dif-author-experiment takes over.
dif-triage-experiments Cloud
Reviewing every active experiment at once and acting on the ones that need it. It reads verdicts
from dif.sh Cloud, so it needs a read token: through the dif MCP server, which you set up with
one, or in DIF_TOKEN for REST. See MCP and API. For REST, the skill reads the base URL from DIF_CLOUD_URL when it is set, and uses https://cloud.dif.sh otherwise.
Triggers on: triage, review all experiments, what needs action, ramp, turn off, kill, winners, or a dif.sh Cloud overview.
The workflow
- calls
get_overviewfor the repo; - shows a table: experiment id, verdict, top reason, next action;
- acts on each row's
next_action.kind, not on the verdict name.
| next_action.kind | what the skill does |
|---|---|
set_weights | Checks that the weights in next_action.file still equal the weights Cloud
analysed. Then it changes only the weight: numbers to next_action.weights and runs dif validate and dif build. |
conclude | Hands off to dif-conclude-experiment with the decision_draft,
unchanged. |
fix_instrumentation | Reports the failed check. Never changes files. |
set_metric_direction | Reports the metric to set in Cloud, on the Metrics page. Never changes files. |
wait | Reports the summary: what is missing, or the ETA in days when Cloud has one. Never changes files. |
Broken data comes back as investigate with fix_instrumentation, so the
skill never acts on it. A harmful variant takes two runs: the first sets control to 100 and every
other variant to 0, and a later run concludes the experiment. The skill builds its git and gh commands only from the checked experiment id, the verdict and
fixed text. The one other text that reaches a shell is the decision: the conclude skill passes it
to dif conclude in single quotes, and stops if it holds a single quote, $(, a
backtick or a line break.
Interactive and autonomous
Interactive is the default: the skill shows each change and asks before it writes. It works on
your current branch and does not commit, push or open a pull request unless you ask. If you ask
for pull requests, it first needs a clean working tree: git status --porcelain --untracked-files=all must print nothing. Otherwise it changes
nothing. Then it follows the branch, commit and pull request rules below, and puts you back on
the branch you started on.
Autonomous mode is for unattended runs only: you tell it to run unattended, or no person can answer, such as a scheduled job. It follows fixed rules:
- It needs a clean working tree before the first change. Otherwise it stops.
- It runs
git fetch originand cuts every branch fromorigin/<default>. It never pulls into, or builds on, your local default branch. At the end it goes back to the branch it started on. After a run that made a branch, rundif buildon your branch. - Just before acting, it calls
get_experiment_resultsfor that experiment. It acts only if the verdict is the same as in the overview andanalysis_computed_atis under 30 minutes old. It reads the clock withnode; withoutnodeit skips the row as stale. - One branch and one pull request per experiment and step:
dif/<verdict>/<id>/w<n>for a weight change, where<n>is the new treatment weight, anddif/<verdict>/<id>/concludefor a conclusion. For exampledif/ramp/new-checkout/w25, ordif/turn_off/new-checkout/w0. - If that branch already exists, it skips the row. With no open pull request the branch is stale: delete it to let the step run again.
- The commit is
chore(dif): <verdict> <id>. It stages only the files the skill changed. The PR title isdif: <verdict> <id>, withurgent:in front forturn_off. - If
git addorgit commitfails, it stops and pushes nothing. If a push fails, it keeps the local branch and reports it. If the pull request cannot be opened, it reports the pushed branch. - The PR body carries the verdict, reasons, caveats,
analysis_computed_atand the exact change. AshipPR says not to merge it before the call sites keep the winning code. - At most 5 pushed branches per run.
- It never merges, never force-pushes and never pushes to the default branch. A person merges.
Agent guidance files
Alongside the skills, dif init writes a managed dif block into CLAUDE.md, AGENTS.md, and .github/copilot-instructions.md.
dif owns only its marked block; anything else you've written in those files is preserved across
re-runs. For Cursor it writes its own project rule, .cursor/rules/dif.mdc, which Cursor
loads when a task is about flags or experiments or a file under dif/ is open. A dif
block left in .cursorrules by an older dif is refreshed; a new .cursorrules is never created.
To install the skills into an agent other than Claude Code, or before running dif init, use the skills CLI: npx skills add dif-sh/dif. It also installs dif-docs, a reference skill
that points at these docs.
The block points the agent at the workflow and at dif/context.json, the session-start
read that lists active experiments, their surfaces and variants, and each surface's most recent
learning. dif build regenerates it, and it lives outside the gitignored dif/generated/ directory so it's checked in and any agent picking up the repo sees the
current state.
dif init --agents none (or the deprecated --no-agent-files alias) to skip the skills and the guidance blocks entirely. The CLI,
SDK, and build are unaffected. The skills are an accelerant for the agent-assisted workflow, not a
dependency of it.