Terraform's killer feature is version control over config — version-pin a known-good state, diff what's changed, roll back when something breaks. weave gets the same property without the baggage of a desired-state file you have to keep in sync.
diff review; confirms or --yes. file.tf ──apply──▶ cloud
│
└── drift means error
The file is the law. Anything different is "drift." Great for greenfield IaC. Painful when reality legitimately changes outside Terraform (someone fixes a port in the dashboard at 3am during an outage).
cloud ──snapshot──▶ ports.yaml
│
cloud ◀───apply──── (your edits)
│
(git tracks it)
Live system is the law. The YAML is a mirror you can version, edit, and push back. Someone fixed a port at 3am? Just resnapshot — the file catches up.
Meraki ports is the canonical example below. The same shape works
across modules: every kind supports snapshot and
diff; apply only where
the module wires live writes for that kind (see each module page).
YAML headers and the Action diff model are shared. See
"Kinds shipping today"
below for the full matrix.
Safe workflow:
snapshot → edit YAML →
diff (read-only, anytime) →
review the panel →
apply --yes in CI or confirm interactively.
Destructive do commands (remove, wipe, suspend, …)
require --yes as well.
$ weave meraki snapshot ports --network "Main Office" Saved .weave-state/meraki/acme-hq/main-office/ports.yaml (1 switch, 24 ports)
The file lands at
.weave-state/<module>/<org-slug>/<network-slug>/<kind>.yaml.
Override the root with WEAVE_STATE_DIR.
Commit the folder to git — that's the whole versioning story.
# weave state file — captured live, edit freely, then `weave apply`.
schema: weave.state/v1
module: meraki
kind: ports
scope:
org_name: Acme HQ
network_id: L_12345
network_name: Main Office
captured_at: '2026-05-15T15:02:13Z'
data:
switches:
- serial: Q2XX-AAA-BBB
name: sw-01
model: MS120-24
ports:
- port_id: "1"
name: Reception
enabled: true
type: access
vlan: 10 # <-- bump to 20
poeEnabled: true
- port_id: "2"
name: Conf-Room # <-- rename + disable
enabled: true
type: access
vlan: 10
poeEnabled: true
...
Use cases that fall out naturally:
bulk port changes (find/replace), swap a switch serial for hardware migration,
review a coworker's pending change as a git diff, restore last week's known-good
state with git checkout.
$ weave meraki diff ports --network "Main Office" Comparing .weave-state/meraki/acme-hq/main-office/ports.yaml against live … Pending changes: +0 ~2 -0 (0 no-op) ~ switch-port Q2XX-AAA-BBB port 1 vlan: 10 → 20 ~ switch-port Q2XX-AAA-BBB port 2 name: 'Conf-Room' → 'Conf-Room-Renamed' enabled: True → False
Diffs are computed field-by-field — you see exactly the fields
that will be touched, never a wall of unchanged YAML.
diff is read-only and
safe to run anytime. Run it after every snapshot to confirm
nothing changed underneath you.
$ weave meraki apply ports --network "Main Office" Comparing .weave-state/meraki/acme-hq/main-office/ports.yaml against live … ╭─ Pending changes ─────────────────────────────────────╮ │ meraki apply ports +0 add ~2 change -0 remove │ ╰───────────────────────────────────────────────────────╯ ~ switch-port Q2XX-AAA-BBB port 1 vlan: 10 → 20 ~ switch-port Q2XX-AAA-BBB port 2 name: 'Conf-Room' → 'Conf-Room-Renamed' enabled: True → False Apply these changes? [y/N]: y Applied meraki ports: +0 ~2 -0
apply always runs
diff first and shows a summary
panel before anything hits the API. With no pending changes it
prints No changes and exits. Pass
--dry-run (same as
diff) to preview only, or
--yes / -y
in CI after you have reviewed the diff. Deletes and whole-document
kinds (ACL, policies) get an extra high-impact warning.
$ git checkout HEAD~1 .weave-state/meraki/acme-hq/main-office/ports.yaml $ weave meraki apply ports --network "Main Office" --yes Pushed 2 update(s) to Meraki. $ git checkout main .weave-state/ # go back to current state file
No state engine. No terraform state ceremony.
The file is the snapshot; git is the history; apply is the
time machine.
Twenty-four state kinds across twenty-three live modules — networking gear, DNS, identity, IAM, MDM, collaboration, observability, dev/ops, secrets, and EDR. More land every week.
All switch ports in a Meraki network, every editable field per port.
weave meraki snapshot ports --network "Main Office"
All appliance VLANs in a network — subnet, DHCP handling, DNS, fixed assignments.
weave meraki snapshot vlans --network HQ
Every DNS record on a zone. The canonical use case: bulk-edit, git-review, apply.
weave cloudflare snapshot dns --zone example.com
Every IPv4 firewall policy on a FortiGate — sources, destinations, services, action.
weave fortinet snapshot policies
Every security rule on the firewall (vsys1) — zones, services, apps, action.
weave paloalto snapshot security-rules
Every WLAN/SSID on a UniFi site — security, VLAN, guest, schedule.
weave unifi snapshot wlans --site default
Every /ip/firewall/filter rule on a RouterOS device.
weave mikrotik snapshot firewall-filter
The whole tailnet ACL document — acls, groups, hosts, tagOwners, ssh — diffed per section.
weave tailscale snapshot acl
Every Okta group with its member logins. Review group-membership changes in a PR.
weave okta snapshot groups
Every Entra directory group with member UPNs (keyed by displayName). Dynamic groups surface as read-only noops.
weave entra snapshot groups
Every Workspace group with its member emails, keyed by primary email. Add / remove / rename via the YAML.
weave googleworkspace snapshot groups
Direct members of a single AD/Entra group (UPNs). Add/remove via git diff.
weave activedirectory snapshot group-memberships --group "IT-Admins"
Every customer-managed IAM policy on the account, keyed by name. JSON bodies inlined.
weave aws_iam snapshot policies
Every device-configuration profile under /deviceManagement, keyed by displayName + @odata.type.
weave intune snapshot configuration-profiles
Every macOS configuration profile in Jamf Pro. XML payloads round-trip inline; apply via Classic API.
weave jamf snapshot configuration-profiles
Snapshot + diff only — Mosyle's API has no profile create/update, so apply tells you to use the dashboard.
weave mosyle snapshot profiles
Branch protection rules on every protected branch of a repo — reviews, checks, restrictions.
weave github snapshot branch-protection --repo org/repo
Every public channel in the workspace — rename, edit topic, archive via the YAML.
weave slack snapshot channels
Every service — name, description, escalation policy — keyed by service name.
weave pagerduty snapshot services
Every project in a Sentry organization — slug, name, platform, team.
weave sentry snapshot projects --org my-org
Every monitor, keyed by name + type. Git-versioned alerts without Terraform.
weave datadog snapshot monitors
Every ACL policy with its HCL body inlined. Single source of truth for your policy set.
weave vault snapshot policies
Every prevention (AV/EDR) policy in the Falcon tenant — settings, host groups, enabled state.
weave crowdstrike snapshot prevention-policies
Coming next on the roadmap (PRs welcome): meraki firewall-rules ·
cloudflare page-rules ·
okta apps ·
jamf policies ·
intune compliance-policies ·
vault auth-methods / mounts ·
aws_iam roles.
.weave-state/
├── meraki/
│ ├── acme-hq/
│ │ ├── main-office/
│ │ │ └── ports.yaml
│ │ └── branch/
│ │ └── ports.yaml
│ └── sandbox/
│ └── lab/
│ └── ports.yaml
├── cloudflare/
│ └── weavewhatever-com/
│ └── dns.yaml
├── fortinet/
│ └── policies.yaml
├── paloalto/
│ └── security-rules.yaml
├── unifi/
│ └── default/
│ └── wlans.yaml
├── mikrotik/
│ └── firewall-filter.yaml
├── tailscale/
│ └── acl.yaml
├── okta/
│ └── groups.yaml
├── entra/
│ └── groups.yaml
├── googleworkspace/
│ └── my-customer/
│ └── groups.yaml
├── activedirectory/
│ └── it-admins/
│ └── group-memberships.yaml
├── aws_iam/
│ └── policies.yaml
├── intune/
│ └── configuration-profiles.yaml
├── jamf/
│ └── configuration-profiles.yaml
├── mosyle/
│ └── profiles.yaml
├── teams/
│ └── teams.yaml
├── github/
│ └── org-repo/
│ └── branch-protection.yaml
├── slack/
│ └── channels.yaml
├── pagerduty/
│ └── services.yaml
├── sentry/
│ └── my-org/
│ └── projects.yaml
├── datadog/
│ └── monitors.yaml
├── vault/
│ └── policies.yaml
└── crowdstrike/
└── prevention-policies.yaml
.weave-state/. Override with WEAVE_STATE_DIR=state/ if you'd rather check the files in under a different path.<module>/<scope-slug>.../<kind>.yaml. Slugs are derived from human-readable names so file paths are skim-friendly.schema: weave.state/v1 block and a scope: map. weave reads these on every diff/apply — never hand-edit them.Each module can opt-in to as many state kinds as makes sense — 24 kinds across 23 modules ship today (see "Kinds shipping today" above). Add a new kind by writing three small functions:
# src/weave/modules/<name>/state.py def snapshot_ports(network: str) -> tuple[dict, dict]: """Pull live state. Return (scope, data) for the YAML header + body.""" ... def diff_ports(desired: dict, live: dict) -> list[Action]: """Compare file vs live. Return one Action per change.""" ... def apply_ports(actions: list[Action]) -> None: """Execute the Actions against the live API.""" ...
Then declare it on the manifest and wire three Typer commands — full example in CONTRIBUTING.md.
depends_on, no providers, no lock file. Each kind round-trips independently.terraform.tfstate — and no central anything. Every state file is independent.
Meraki ports is the canonical
first example. Wire up the next kind on a module you care about
— open a PR or
file an issue
with the kind you want.