Move on Sui and Aptos skill for Codex
Minimal abilities, explicit capabilities, tested aborts. Codex designs Move modules starting from abilities, keeping store, copy and drop off capabilities and assets, and chooses owned over shared objects on Sui unless coordination demands sharing. It writes Move 2024 code for Sui and Move 2 for Aptos, tests every abort code, binds capabilities to the objects they control, and asks before any mainnet publish or upgrade.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- move-language Installs to ~/.agents/skills/move-language/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install move-language The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project move-language Installs to .agents/skills/move-language/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/move-language.zipunzip -o move-language.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) move-language PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $move-language, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes, tests and upgrades Move smart contracts on Sui (Move 2024 edition, objects with UID, owned vs shared objects, capabilities, one-time witness, test_scenario, sui client publish and upgrade) and Aptos (Move 2, resources in global storage, objects, Fungible Asset, view functions, aptos move test and publish), with ability design and access-control review. Use when asked to build or review a Sui or Aptos Move module, choose owned versus shared objects, design capabilities, write Move unit tests, publish a package, or plan a compatible upgrade.
Use it when
- You are building or reviewing a Move package on Sui or Aptos
- Abilities, capabilities or owned versus shared objects need deciding
- Move unit tests or expected_failure tests are missing
- A package publish or compatible upgrade is being planned
Use something else when
- The program targets Solana with Rust: use solana-anchor
- The contract targets an EVM chain in Solidity: use solidity-contracts
How to check the result
sui move test or aptos move test passes with an expected_failure test per abort code, and the review checklist finds no capability with store or unbound object IDs.
The mistake it prevents
Giving a capability struct the store ability, which lets anyone holding it transfer admin power freely with public_transfer.
A Move package is right when abilities are minimal (no store or copy a type does not
need), every privileged function takes a capability or checks ownership, shared state is
used only where coordination is truly required, each abort has a named constant, tests cover
every abort, and the upgrade policy and its authority are deliberate.
Inspect first
- Read
AGENTS.mdandMove.toml: which chain (Sui or Aptos),edition(Sui"2024"), named addresses, framework dependency and upgrade policy. - Check the CLI:
sui --versionoraptos --version. Framework dependencies are fetched from git on first build, which needs network; recent Sui CLIs add the system packages implicitly. - Baseline:
sui move build && sui move testoraptos move compile && aptos move test.
Sui vs Aptos at a glance
| Concept | Sui | Aptos |
|---|---|---|
| State | Objects (has key, first field id: UID) owned by an address, shared, or wrapped | Resources stored under an address (move_to), plus objects |
| Authorization | Owning the object or holding a capability object | &signer plus stored ownership checks |
| Entry points | public fun (composable in programmable transaction blocks), entry fun | public entry fun; #[view] for reads |
| Fungible tokens | Coin<T> / Balance<T> created with a one-time witness | Fungible Asset standard (primary_fungible_store) |
| Package visibility | public(package) | public(package) (Move 2) or friend |
| Upgrades | UpgradeCap object; compatible-only by default | upgrade_policy in Move.toml; publisher account upgrades |
Abilities: decide deliberately
| Ability | Grants | Default stance |
|---|---|---|
key | Can be a top-level object/resource | Required for objects and resources |
store | Can be stored inside others and, on Sui, transferred freely by anyone holding it (public_transfer) | Omit on capabilities and soulbound items unless free transfer is intended |
copy | Can be duplicated | Never on assets, receipts or capabilities |
drop | Can be discarded silently | Never on assets or hot-potato receipts that must be consumed |
A struct with no abilities (“hot potato”) must be consumed in the same transaction:
use it for flash loans so the borrower must call repay.
Sui module (Move 2024)
module counter::counter;
use sui::event;
const ENotOwner: u64 = 0;
public struct Counter has key {
id: UID,
owner: address,
value: u64,
}
public struct AdminCap has key { id: UID }
public struct Incremented has copy, drop { counter: ID, value: u64 }
fun init(ctx: &mut TxContext) {
transfer::transfer(AdminCap { id: object::new(ctx) }, ctx.sender());
}
public fun create(ctx: &mut TxContext) {
transfer::share_object(Counter { id: object::new(ctx), owner: ctx.sender(), value: 0 });
}
public fun increment(c: &mut Counter) {
c.value = c.value + 1;
event::emit(Incremented { counter: object::id(c), value: c.value });
}
public fun set_value(c: &mut Counter, value: u64, ctx: &TxContext) {
assert!(c.owner == ctx.sender(), ENotOwner);
c.value = value;
}
public fun value(c: &Counter): u64 { c.value }
Sui design rules:
- Owned objects first. Transactions touching only owned objects skip consensus and are faster. Share an object only when many users must mutate it (pools, order books), and expect contention on hot shared objects.
- Capabilities over address checks for admin actions: a function that takes
_: &AdminCapcan only be called by whoever owns that object. Withoutstore, the cap can only be transferred by this module’s code. - Bind objects that belong together: when a cap controls one specific pool, store the
pool’s
IDin the cap and assertobject::id(pool) == cap.pool_id. - One-time witness (OTW): a struct named after the module in uppercase with only
drop, received byinit; used for coin creation andpackage::claim(Publisher). Check the current recommended coin-creation API in the Sui framework docs for your CLI version. initruns once at publish and never on upgrade. Migrations need an explicit admin function.
Sui tests
#[test_only]
module counter::counter_tests;
use sui::test_scenario;
use counter::counter::{Self, Counter};
#[test]
fun increments_shared_counter() {
let alice = @0xA;
let mut scenario = test_scenario::begin(alice);
counter::create(scenario.ctx());
scenario.next_tx(alice);
let mut c = scenario.take_shared<Counter>();
counter::increment(&mut c);
assert!(counter::value(&c) == 1, 0);
test_scenario::return_shared(c);
scenario.end();
}
#[test, expected_failure(abort_code = counter::ENotOwner)]
fun stranger_cannot_set_value() {
let (alice, bob) = (@0xA, @0xB);
let mut scenario = test_scenario::begin(alice);
counter::create(scenario.ctx());
scenario.next_tx(bob);
let mut c = scenario.take_shared<Counter>();
counter::set_value(&mut c, 9, scenario.ctx());
test_scenario::return_shared(c);
scenario.end();
}
Sui commands
sui move new counter
sui move build
sui move test # add --coverage, then `sui move coverage summary`
sui client publish # needs network, active env and gas
sui client upgrade --upgrade-capability <UPGRADE_CAP_ID>
Publishing and upgrading on mainnet need explicit user confirmation: show the active
environment (sui client active-env), address and package first. Store the UpgradeCap with
a multisig, or make the package immutable deliberately (sui::package::make_immutable) only
when the user asks for that, since it cannot be undone.
Aptos module (Move 2)
module counter_addr::counter {
use std::signer;
use aptos_framework::event;
const E_NOT_INITIALIZED: u64 = 1;
struct Counter has key { value: u64 }
#[event]
struct Incremented has drop, store { owner: address, value: u64 }
public entry fun init_counter(account: &signer) {
move_to(account, Counter { value: 0 }); // aborts if it already exists
}
public entry fun increment(account: &signer) acquires Counter {
let addr = signer::address_of(account);
assert!(exists<Counter>(addr), E_NOT_INITIALIZED);
let c = borrow_global_mut<Counter>(addr);
c.value = c.value + 1;
event::emit(Incremented { owner: addr, value: c.value });
}
#[view]
public fun value(addr: address): u64 acquires Counter {
borrow_global<Counter>(addr).value
}
#[test(account = @0xcafe)]
fun test_increment(account: &signer) acquires Counter {
init_counter(account);
increment(account);
assert!(value(signer::address_of(account)) == 1, 0);
}
#[test(account = @0xcafe)]
#[expected_failure(abort_code = E_NOT_INITIALIZED, location = Self)]
fun test_increment_without_init(account: &signer) acquires Counter {
increment(account);
}
}
Aptos design rules:
- A resource can only be moved to an address by a
&signerfor that address; but anypublicfunction can read any address’s resources. Never treat “stored at an address” as private. - Use objects (
object::create_named_object,ObjectGroup) for transferable, composable assets; use the Fungible Asset standard for new tokens rather than the legacycoinmodule. - Store
SignerCapability/ExtendRefonly in resources your module guards; leaking one hands over the account or object. publicfunctions are permanent API undercompatibleupgrade policy; keep internals inpublic(package)or private functions.
Aptos commands
aptos init --network devnet # creates a profile; needs network
aptos move compile --named-addresses counter_addr=default
aptos move test --named-addresses counter_addr=0xcafe
aptos move prove --named-addresses counter_addr=0xcafe # Move Prover, if specs exist
aptos move publish --named-addresses counter_addr=default
Mainnet publishes need explicit user confirmation of profile, address and package.
Upgrade compatibility (both chains)
- Allowed: new modules, new functions, new structs, changing function bodies.
- Not allowed under compatible policy: removing or changing public function signatures, changing struct fields or abilities of existing structs.
- To evolve shared state, add a
versionfield to shared objects or resources and gate functions on it, with an adminmigratefunction.
Review checklist
- No
store/copy/dropon capabilities, receipts or assets that should not have them - Every mutating function on shared objects checks ownership or a capability
- Capabilities are bound to the specific object they control
- Division by zero and
u64overflow aborts cannot freeze critical paths - Abort codes are named constants and each one has an
expected_failuretest - Events emitted for state changes indexers need
- Upgrade authority held by a multisig or deliberately removed
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Move on Sui and Aptos"
short_description: "Minimal abilities, explicit capabilities, tested aborts"
default_prompt: "$move-language Write a Sui Move module for a shared marketplace with an admin capability" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.