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.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- move-language

Installs to ~/.agents/skills/move-language/ for every project on this machine.

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

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.

~/.agents/skills/move-language/SKILL.md

--- name: move-language description: Writes, tests and upgrades Move smart contracts on Sui (Move 2024 edition, objects with UID, owned vs shared objects, capabilities, one-time witness,… license: MIT ---

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

  1. Read AGENTS.md and Move.toml: which chain (Sui or Aptos), edition (Sui "2024"), named addresses, framework dependency and upgrade policy.
  2. Check the CLI: sui --version or aptos --version. Framework dependencies are fetched from git on first build, which needs network; recent Sui CLIs add the system packages implicitly.
  3. Baseline: sui move build && sui move test or aptos move compile && aptos move test.

Sui vs Aptos at a glance

ConceptSuiAptos
StateObjects (has key, first field id: UID) owned by an address, shared, or wrappedResources stored under an address (move_to), plus objects
AuthorizationOwning the object or holding a capability object&signer plus stored ownership checks
Entry pointspublic fun (composable in programmable transaction blocks), entry funpublic entry fun; #[view] for reads
Fungible tokensCoin<T> / Balance<T> created with a one-time witnessFungible Asset standard (primary_fungible_store)
Package visibilitypublic(package)public(package) (Move 2) or friend
UpgradesUpgradeCap object; compatible-only by defaultupgrade_policy in Move.toml; publisher account upgrades

Abilities: decide deliberately

AbilityGrantsDefault stance
keyCan be a top-level object/resourceRequired for objects and resources
storeCan 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
copyCan be duplicatedNever on assets, receipts or capabilities
dropCan be discarded silentlyNever 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 _: &AdminCap can only be called by whoever owns that object. Without store, 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 ID in the cap and assert object::id(pool) == cap.pool_id.
  • One-time witness (OTW): a struct named after the module in uppercase with only drop, received by init; used for coin creation and package::claim (Publisher). Check the current recommended coin-creation API in the Sui framework docs for your CLI version.
  • init runs 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 &signer for that address; but any public function 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 legacy coin module.
  • Store SignerCapability/ExtendRef only in resources your module guards; leaking one hands over the account or object.
  • public functions are permanent API under compatible upgrade policy; keep internals in public(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 version field to shared objects or resources and gate functions on it, with an admin migrate function.

Review checklist

  • No store/copy/drop on 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 u64 overflow aborts cannot freeze critical paths
  • Abort codes are named constants and each one has an expected_failure test
  • 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.yamlgenerated from this page

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.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem