Upgrade-safety checker for Solana programs.
ratchet compares a new program surface against the deployed program on-chain (or a committed ratchet.lock baseline) and flags changes that would silently corrupt data, break clients, or orphan PDAs — before the upgrade transaction lands.
ratchet ships with a SKILL.md at the repo root — a
frontmattered skill definition any Claude-style agent can load to know
when to invoke ratchet, which flags to use, and how to interpret
findings. The same file is served at /skill.md on the website when
deployed.
Solana program upgrades have no equivalent of buf breaking or @openzeppelin/hardhat-upgrades. Today a developer can rename an #[account] struct, silently change the discriminator, and orphan every account the program owns — solana program upgrade will happily land it. ratchet closes that gap.
Every diff is classified as:
| Verdict | Exit | Meaning |
|---|---|---|
ADDITIVE |
0 |
Backward-compatible. Existing accounts and clients keep working. |
UNSAFE |
2 |
Needs a declared migration or --unsafe-* acknowledgement. |
BREAKING |
1 |
Will corrupt on-chain state, break existing clients, or orphan existing PDAs. |
Alpha — production-shaped, but pre-1.0. What ships today:
- 27 rules: 20 R-rules (diff-time) + 7 P-rules (preflight readiness).
- Anchor IDL adapter — file loader, RPC fetcher with auto-derived
IDL account address from
--program, on-chain account decoder. - Quasar IDL adapter — parses + normalises Quasar's
variable-discriminator JSON shape so every rule applies identically.
Auto-detects
Quasar.toml; explicit--quasarflag for non-cwd cases. - Codama IDL adapter — normalises Codama
rootNodeIDLs (the node-based standard used by native and Pinocchio programs via Shank, and by Codama-converted Anchor programs). Self-identifying: any local IDL path whose JSON is arootNoderoutes to the Codama loader automatically, no flag needed. Anchor-origin byte discriminators and Shank-style u8 index discriminators both resolve, andpdaNodeseeds flow into R013. ratchet.lockcommittable baselines;syn-based Anchor source parser that fills in PDA seeds the IDL lost.ratchet replay— samples live program accounts via RPC and flags ones that don't match the new IDL's minimum layout. Optional LiteSVM deploy smoke test under thelitesvm-deployfeature.ratchet squads— decodes Squads V4 vault-transaction proposals and optionally runscheck-upgradeagainst the proposal's target program.- GitHub Actions:
action.yml(Anchor) +action-quasar.yml(Quasar). - Human + JSON output everywhere, CI-friendly exit codes (0 safe, 1 breaking, 2 unsafe).
Coming next: stable Quasar __QUASAR_SCHEMA binary reader once
upstream PR lands;
deeper compiler-pass integration when Quasar exposes a plugin surface
(see docs/quasar-integration.md for our
roadmap).
cargo install solana-ratchet-cliThe binary is called ratchet. Local install from a checkout:
cargo install --path crates/ratchet-cliLibrary crates publish under the solana-ratchet-* prefix
(solana-ratchet-core, solana-ratchet-anchor, solana-ratchet-lock,
solana-ratchet-source, solana-ratchet-svm, solana-ratchet-squads,
solana-ratchet-quasar, solana-ratchet-codama).
ratchet check-upgrade \
--old target/idl/vault.json \
--new target/idl/vault.json.new# Snapshot the current surface into ratchet.lock (run once per release)
ratchet lock --from-idl target/idl/vault.json --out ratchet.lock
# In CI, on every PR:
ratchet check-upgrade --lock ratchet.lock --new target/idl/vault.json# Auto-derive the Anchor IDL account address from the program id
ratchet check-upgrade \
--program <PROGRAM_ID> \
--cluster mainnet \
--new target/idl/vault.json
# Or point at an explicit IDL account (e.g. for programs with custom layouts)
ratchet check-upgrade \
--idl-account <IDL_ACCOUNT_PUBKEY> \
--cluster mainnet \
--new target/idl/vault.jsonAnchor 0.30+ IDLs capture PDA seeds but sometimes flatten account-field references. Point ratchet at your program source to parse #[account(seeds = [...])] directly:
ratchet check-upgrade --lock ratchet.lock --new target/idl/vault.json \
--new-source programs/vault/srcUsing ratchet with Quasar
Quasar emits an IDL JSON at target/idl/<program>.json after quasar build. Same workflow as Anchor — point ratchet readiness / check-upgrade at the file. Inside a Quasar workspace, ratchet detects Quasar.toml and switches loaders automatically:
# From a Quasar repo root — autodetect picks the Quasar parser.
quasar build
ratchet readiness --new target/idl/escrow.json
ratchet check-upgrade \
--old examples/quasar/escrow.json \
--new target/idl/escrow.jsonOutside a Quasar workspace, force the Quasar parser explicitly:
ratchet readiness --new path/to/quasar.json --quasar
ratchet check-upgrade --old old.json --new new.json --quasarTry the committed examples without installing Quasar:
ratchet readiness --new examples/quasar/escrow.json --quasar
ratchet check-upgrade \
--old examples/quasar/escrow.json \
--new examples/quasar/escrow.v2.json \
--quasarThe same R-rule + P-rule catalog applies; only the loader differs. Two semantics notes:
- Quasar uses short (typically 1-byte) discriminators. Ratchet's IR
stores discriminators at their exact length —
[0x05]and[0x05, 0x00]are different wire formats, so R006/R014/R016 fire on length changes too, and P007 flags selectors that prefix each other. - P003 / P004 (default-discriminator-pin) stay silent on Quasar.
Quasar devs always assign discriminators explicitly
(
#[instruction(discriminator = N)]), so the "is this the Anchor sha256 default?" check is a category error there.
A drop-in CI workflow lives at action-quasar.yml. The roadmap for matching Quasar's evolution (binary __QUASAR_SCHEMA, eventual plugin API) is in docs/quasar-integration.md.
Codama IDLs describe native and Pinocchio programs (usually generated via Shank) in a node-based JSON format. They're self-identifying — the file starts with "kind": "rootNode" — so ratchet routes them automatically:
ratchet readiness --new codama/idl.json
ratchet check-upgrade --old codama/idl.v1.json --new codama/idl.json
ratchet lock --from-idl codama/idl.jsonAnchor-origin 8-byte discriminators and Shank-style 1-byte instruction indexes both normalize with their exact lengths; SPL-style size-discriminated accounts (no byte selector) are handled too. Borsh-equivalent Codama type nodes map onto ratchet's IR; exotic encodings (big-endian numbers, fixed COption, maps/sets, hidden prefixes) stay diffable as opaque raw nodes.
Diff Codama against Codama: cross-format comparisons (an Anchor IDL vs its Codama conversion) are unsupported — the formats use different name conventions (PascalCase/snake_case vs camelCase), so everything reports as removed+added.
ratchet replay --program <PROGRAM_ID> \
--new target/idl/vault.json \
--limit 500 \
--so target/deploy/vault.soPulls up to 500 program-owned accounts via getProgramAccounts, classifies each by the Anchor discriminator, and flags any whose data is shorter than the new IDL's minimum layout. Optional --so verifies the candidate binary's ELF header (magic, 64-bit, little-endian, SBF/SBPF shared object) before sampling — catches pushes of the wrong target build.
# Basic classification
ratchet squads --proposal <VAULT_TX_PUBKEY> --cluster mainnet
# Full signer experience: decode + fetch current IDL + run check-upgrade
ratchet squads --proposal <VAULT_TX_PUBKEY> \
--auto-diff --new target/idl/vault.json
# Verify the buffer the proposal deploys is exactly the artifact you reviewed
ratchet squads --proposal <VAULT_TX_PUBKEY> \
--verify-so target/deploy/vault.soFull Borsh decode pulls the concrete program_id and buffer pubkeys straight off the CompiledInstruction. With --auto-diff, ratchet fetches the current on-chain IDL for the proposal's target program and runs check-upgrade against the candidate IDL you provide — the signer sees the exact schema diff before clicking approve. With --verify-so, ratchet fetches the proposal's buffer account and sha256-compares its program bytes against your locally built .so (any non-zero bytes past the artifact length also fail) — a mismatch exits 1, so "the buffer is the file I reviewed" becomes a checkable precondition for signing.
cargo install solana-ratchet-cli --features litesvm-deploy
ratchet replay --program <PID> --new target/idl/vault.json \
--so target/deploy/vault.so --deploy--deploy loads the .so into an in-process LiteSVM to confirm the runtime accepts the bytecode. The feature is opt-in because LiteSVM pulls in the Solana runtime crates; default builds stay lightweight and use the ELF-header check.
# Demote the R006 finding for a deliberate struct rename
ratchet check-upgrade --lock ratchet.lock --new new.json \
--unsafe allow-rename
# Declare a Migration<From, To> for the Vault account — demotes
# R003 (removed), R004 (mid-insert), R005 (append) for that account.
ratchet check-upgrade --lock ratchet.lock --new new.json \
--migrated-account Vault
# Declare an Anchor realloc constraint for the Vault account — demotes
# R005 (append) for that account. Auto-detected when --new-source is
# provided and the field carries #[account(mut, realloc = ...)].
ratchet check-upgrade --lock ratchet.lock --new new.json \
--realloc-account Vaultratchet --json check-upgrade --lock ratchet.lock --new new.json \
| jq '.findings[] | select(.severity != "additive")'ratchet list-rulesA composite action is shipped from the repo root (action.yml). On every PR, diff the candidate IDL against a committed ratchet.lock:
- uses: saicharanpogul/ratchet@v0.4.0
with:
new: target/idl/my_program.json
lock: ratchet.lockSee examples/github-workflow.yml for a complete example including Rust toolchain setup and caching. Action outputs verdict (safe / breaking / unsafe) and exit-code for downstream steps to react to.
| ID | Name | Severity | Allow flag |
|---|---|---|---|
| R001 | account-field-reorder | BREAKING | — |
| R002 | account-field-retype | BREAKING | allow-type-change |
| R003 | account-field-removed | BREAKING | allow-field-removed or --migrated-account |
| R004 | account-field-insert-middle | BREAKING | allow-field-insert or --migrated-account |
| R005 | account-field-append | UNSAFE | allow-field-append, --realloc-account, or --migrated-account |
| R006 | account-discriminator-change | BREAKING | allow-rename |
| R007 | instruction-removed | BREAKING | allow-ix-removal |
| R008 | instruction-arg-change | BREAKING | allow-ix-arg-change |
| R009 | instruction-account-list-change | BREAKING | allow-ix-account-change |
| R010 | instruction-signer-writable-flip | BREAKING (tightening) / ADDITIVE (relaxation) | allow-signer-mut-flip |
| R011 | enum-variant-removed-or-inserted | BREAKING | — |
| R012 | enum-variant-append | ADDITIVE | — (informational) |
| R013 | pda-seed-change | BREAKING | allow-pda-shape-change (presence flip only) |
| R014 | instruction-discriminator-change | BREAKING | allow-ix-rename |
| R015 | account-removed | BREAKING | allow-account-removal |
| R016 | event-discriminator-change | BREAKING | allow-event-rename |
| R017 | event-removed | BREAKING | allow-event-removal |
| R018 | error-code-change | UNSAFE | allow-error-change |
| R019 | type-shape-change | BREAKING | allow-type-shape-change |
| R020 | serialization-change | BREAKING | allow-serialization-change |
Pass an allow flag with --unsafe <flag> (e.g. --unsafe allow-rename). Declare migration coverage with --migrated-account <Name> or --realloc-account <Name> — both demote R005 appends to Additive, and --migrated-account also demotes R003/R004 since a declared migration can rewrite accounts to any layout.
A vault program in which Vault has had its fields reordered, a new bump field appended, its discriminator changed, the withdraw instruction removed, the deposit argument type changed from u64 to u32, and a new enum variant inserted in the middle:
BREAKING R001 account-field-reorder account:Vault
fields reordered in account `Vault`: [owner, balance] → [balance, owner]
UNSAFE R005 account-field-append account:Vault/field:bump
field `Vault.bump` (u8) appended; existing accounts lack these bytes...
(acknowledge with --unsafe allow-field-append)
BREAKING R006 account-discriminator-change account:Vault/discriminator
discriminator of account `Vault` changed: 0xd308e82b02987577 → 0x6363636363636363
BREAKING R007 instruction-removed ix:withdraw
instruction `withdraw` was removed...
BREAKING R008 instruction-arg-change ix:deposit/args
argument signature of `deposit` changed: (amount: u64) → (amount: u32)
BREAKING R011 enum-variant-removed-or-inserted type:Side/variant:Cross
enum variant `Side::Cross` inserted before existing variants...
verdict: BREAKING — upgrade will corrupt data or break clients
Exit code 1.
ratchet/
├── action.yml # GitHub Action (composite)
├── SKILL.md # agent-discoverable skill definition
├── crates/ # Rust workspace (all 8 crates publish under solana-ratchet-*)
│ ├── ratchet-core/ # framework-agnostic IR and rule engine
│ ├── ratchet-anchor/ # Anchor IDL loader, decoder, normalizer, RPC fetch, PDA derivation
│ ├── ratchet-lock/ # ratchet.lock format
│ ├── ratchet-source/ # syn-based source parser for PDA seeds + realloc constraints
│ ├── ratchet-svm/ # sample-account runtime verification + ELF header check
│ ├── ratchet-squads/ # Squads V4 vault-transaction decoder
│ ├── ratchet-quasar/ # compiler-pass entry points and SurfaceBuilder
│ └── ratchet-cli/ # the `ratchet` binary
├── web/ # Next.js 15 frontend (landing, /diff, /rules, /skill.md)
├── examples/
│ └── github-workflow.yml
├── docs/
│ ├── publishing.md
│ └── quasar-integration.md
└── ...
Apache-2.0. See LICENSE.