Warp Update — Generic Orchestrator

SkillSearch

Generic update orchestrator for an already-deployed warp route. Reads a Linear ticket describing any combination of warp-fee changes / ISM changes / hook changes / owner-address changes / additional-collateral-chain additions, edits the registry deploy.yaml accordingly, builds a per-chain strategy file from the artifact-context owner classifications, runs `hyperlane warp apply`, fork-simulates the resulting receipts via `/warp-route-check`, and hands off to `/warp-update-propose` to land the proposals. Chain extension (adding a brand-new chain with new on-chain contracts) is a separate skill — see `/warp-update-extend`.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Warp Update — Generic Orchestrator skill

What this skill tells your AI

The instructions your AI receives, as published by hyperlane-xyz/hyperlane-monorepo in .claude/skills/warp-update/SKILL.md and read by ahel’s review.

For UPDATES to an already-deployed warp route. Batches any combination of these change types in a single run:

  • Warp Fee ChangetokenFee.feeContracts.<chain>.bps, fee owner, fee maxFee / halfAmount
  • ISM Change<chain>.interchainSecurityModule: switching ISM type (default fallback → aggregation → multisig), adding/removing modules from an aggregation, changing rate-limit caps, toggling pausable state, etc.
  • Hook Change<chain>.hook: switching hook type, adjusting amount-routing thresholds, swapping fallback hooks, etc.
  • Owner Change — chain-level owner, fee-contract owner, ISM/hook owner, ProxyAdmin owner; any post-deploy ownership rotation
  • Additional Collateral Chain — a new <chain> entry with type: collateral plus the corresponding extension of tokenFee.feeContracts (for multi-collateral routes)

For a brand-new warp route deployment use /warp-deploy-init-route. For adding a new chain that doesn't yet have any of the route's contracts on it use /warp-update-extend (it has the extra deploy-on-new-chain step this skill skips).

Run Log (mandatory)

Maintain the durable, per-ticket run log per /warp-run-log — that skill owns the storage contract (Linear-document-by-title primary, single-writer discipline, local-file fallback), the machine-row + prose entry shape, and the surface-the-URL-as-proof hard gate. Use warp-update as the skill name in each prose entry, and do not report this skill complete until the run-log URL has been surfaced.

Log at least: (a) skill entry with the ticket ID + warp route ID + the change types detected, (b) every [CONFIRM:] gate — before and after the response, (c) the surgical deploy.yaml edits made, (d) the warp apply run (any newly deployed contract addresses + tx/receipt refs), (e) the fork-simulate-verify verdict from /warp-route-check, (f) the propose handoff (batch → signer per /warp-update-propose), (g) the registry PR URL, (h) skill exit (success or bail-out). Log smooth steps too — success data grounds the retrospective as much as failure data.

Input

  • Linear ticket ID (required, e.g. AW-123)
  • Warp route ID (required, e.g. ETH/arbitrum-base)
  • Receipts directory (optional) — where warp apply writes per-chain Safe TX Builder / AltVMFile receipts. Defaults to a tmp path derived from the ticket and warp route ID.

If the ticket ID or warp route ID is missing, ask the user.

Prerequisite skills

This skill depends on three artifacts and three skill chains:

  1. /warp-deploy-select-keys <ticket-id> must have produced ~/.hyperlane/key-contexts/<ticket-id>.yaml. Required because warp apply may deploy new contracts during an update (new ISM, new hook, new fee contract on a chain that didn't have fees before, new router for an added collateral chain) and the deployer key signs those deploys via jsonRpc before any multisig sees a tx.
  2. /warp-update-resolve-artifacts <ticket-id> <warp-route-id> must have produced ~/.hyperlane/update-context/<ticket-id>.yaml. Required because the strategy file in Step 4 needs the per-artifact owner classification to dispatch each submission to the right submitter type.
  3. /warp-deploy-fund-deployer <ticket-id> must have run successfully in Step 5 below. Required because the deployer's address needs sufficient balance on every chain that may see new deploys.

If any of those artifacts is missing when this skill starts, invoke the corresponding skill first.


Step 1: Read the Linear Ticket and Identify Requested Changes

Fetch the ticket per /fetch-linear-ticket. Parse the returned description for change instructions. Common targets:

Ticket languagedeploy.yaml target
"Update warp fee on <chain> to N bps" / fee-direction changestokenFee.feeContracts.<chain>.bps (on the synthetic chain's tokenFee)
"Change fee owner to <addr>"tokenFee.feeContracts.<chain>.owner (per-chain) or tokenFee.owner (RoutingFee-level)
"Change ISM to / add module / remove module / pause / set rate limit"<chain>.interchainSecurityModule — flat address or nested object; see Step 1a
"Change hook to / set threshold / swap fallback"<chain>.hook — flat address or nested object; see Step 1a
"Rotate chain owner on <chain> to <addr>"<chain>.owner
"Rotate ProxyAdmin owner on <chain> to <addr>"<chain>.proxyAdmin.owner
"Add <chain> as additional collateral"new <chain>: entry with type: collateral + tokenFee.feeContracts.<chain>: { … }

If the ticket combines multiple changes, parse them all. The skill applies the batched edits in a single warp apply run.

1c: xERC20 routes — token ownership is NOT a warp apply change

On an xERC20 / xERC20-lockbox route, the warp config's owner is the router owner. warp apply manages the router / ISM / hook / rate-limit config only — it does not transfer the underlying xERC20 token's ownership or its ProxyAdmin. Those are a separate contract (xerc20:token / xerc20:proxyAdmin — see /warp-update-resolve-artifacts) and move via hyperlane xerc20 apply, which is config-driven and submitted through the same strategy/submitter machinery (a Safe / ICA / timelock owner all work — do NOT route xERC20 ownership through the infra check-deploy path). In that command's config the top-level owner is the token owner, and the ProxyAdmin owner resolves as ownerOverrides.proxyAdmin ?? proxyAdmin.owner ?? owner (unspecified → token and ProxyAdmin both go to owner). Never conflate the two owner meanings: it is the router owner under warp apply, the token owner under xerc20 apply. The token may also be externally governed or non-Ownable (per /warp-update-resolve-artifacts), in which case we can't transfer it — surface that instead of emitting a doomed transferOwnership.

If a run changes both xERC20 bridge limits (setBufferCap / addBridge / setRateLimitPerSecond) AND token ownership, sequence limits/bridges first, ownership handoff last — both are onlyOwner, so once ownership moves to a Safe/timelock the deployer can no longer set limits.

1a: ISM and Hook Targeting (flat vs nested)

interchainSecurityModule and hook can each be either a flat string address (for "use mailbox default" or an existing custom contract) OR a nested object describing the on-chain structure. Common nested shapes:

  • staticAggregationIsm with modules: [...] (each module potentially a rateLimitedIsm, pausableIsm, defaultFallbackRoutingIsm, etc.)
  • amountRoutingHook with domains: { <chain>: <inner-hook> }, lowerHook, upperHook, threshold
  • Routing variants (domainRoutingIsm, fallbackRoutingHook) with domains: { <chainName>: <inner> }

For the target field, read the current deploy.yaml first (Step 2) and identify the actual on-chain shape. Then edit the specific node — e.g. for "set rate limit on solanamainnet to 200M", find the rateLimitedIsm node inside the aggregation tree on solanamainnet and update its maxCapacity field. Don't guess the structure — verify against the current state.

The same logic applies to hook updates: find the relevant node (lower/upper hook, a specific domain's routing entry, a fallback hook) and update it surgically. See the nested-tree walk pattern documented in /warp-update-resolve-artifacts Step 3.

Before committing to a new ISM or hook structure, research the semantics yourself. It is extremely easy to brick a warp route by misconfiguring an ISM or a hook — a deploy.yaml typo that survives warp apply and lands on chain costs everyone hours of recovery. The skill intentionally does not enumerate every safe composition; the right shape depends on what the ticket wants. Do the work:

For any new ISM module or hook component you are about to add, remove, or recompose:

  1. Read the source. solidity/contracts/isms/<Name>.sol or solidity/contracts/hooks/<Name>.sol. Walk verify() (ISM) or postDispatch() / quoteDispatch() (hook) line-by-line under the new config.
  2. Trace both paths. Normal path: is the security / cost / gas-payment guarantee you expect actually enforced? Emergency path (paused, rate-limited, threshold crossed): is the failure mode what the ticket wants (revert / reroute / drop)?
  3. Aggregation math. staticAggregationIsm with threshold = modules.length is AND across modules; threshold: 1 is OR. Pick deliberately and confirm it matches the ticket's intent.
  4. Preserve removed guarantees. Replacing 0x0 (mailbox default) means removing whatever the mailbox default was enforcing — validator set, IGP gas payment, merkle-tree insertion. Compose so the guarantee survives — commonly via defaultFallbackRoutingIsm inside aggregation for ISMs, or aggregationHook / fallbackRoutingHook retaining the default hook for hooks.

Brick modes to specifically check against (non-exhaustive — there are more):

  • No-op ISM standalone. Some ISMs (e.g. pausableIsm) have verify() that returns true unconditionally when their gate is open — they do no actual verification. Used alone, every message delivers. They MUST be composed with a real verifying ISM via aggregation.
  • Always-reverting ISM standalone. Some ISMs have preconditions (e.g. rateLimitedIsm requires the message to already be _isDelivered) that are false during normal verify-time. Used alone, no message can be delivered.
  • Threshold-1 with a guard module. staticAggregationIsm({ pausableIsm, defaultFallbackRoutingIsm }, threshold: 1) is broken — the pause never gates anything because the default still passes. Threshold must equal the count of modules you want to AND together.
  • Hook that swallows IGP payment. Replacing the mailbox-default hook (which typically includes the InterchainGasPaymaster) with a single-purpose hook means gas is not paid and the relayer never picks up the message. Compose new hooks via aggregationHook / fallbackRoutingHook so IGP payment stays on the success path.
  • Amount-routing threshold misconfigured. amountRoutingHook with an unrealistic threshold silently misroutes every dispatch (too high → all transfers go to upperHook; too low → all go to lowerHook).

When uncertain about a composition you have not used before, surface the question to the user with the relevant contract excerpt rather than guessing.

Canonical schema files — read these when authoring or editing nested ISM / hook / fee configs:

  • ISMs: typescript/sdk/src/ism/types.tsIsmConfigSchema union, plus per-type schemas (PausableIsmConfigSchema, RateLimitedIsmConfigSchema, AggregationIsmConfigSchema, RoutingIsmConfigSchema, etc.). The MUTABLE_ISM_TYPE constant enumerates which types can be edited in place vs. which require a fresh deploy on any change.
  • Hooks: typescript/sdk/src/hook/types.tsHookConfigSchema union, plus per-type schemas. MUTABLE_HOOK_TYPE is the equivalent in-place-editable list.
  • Fees: typescript/sdk/src/fee/types.tsTokenFeeConfigSchema discriminated union (LinearFee, RoutingFee, CrossCollateralRoutingFee, etc.); note that LinearFee.bps is immutable so bps edits trigger a redeploy.
  • Top-level token / per-chain router shape: typescript/sdk/src/token/types.tsHypTokenRouterConfigSchema and the HypTokenConfig token-type union.
  • Shared mixins: typescript/sdk/src/types.tsOwnableSchema (owner + optional ownerOverrides) and PausableSchema (Ownable + paused: boolean). Many ISM / hook configs .extend(OwnableSchema) or .and(PausableSchema), so owner is required on more types than the field name alone would suggest (every routing-variant ISM, the offchain-lookup ISM, the interchainAccountRouting ISM, the rate-limited ISM optionally, pausableIsm, etc.).

Reference existing production deploy.yamls using the same composition — grep deployments/warp_routes/*/*-deploy.yaml in the registry for the pattern you want. Copy the canonical shape; missing required fields (e.g. owner or domains: {} on defaultFallbackRoutingIsm, paused: false on pausableIsm) get rejected at Zod parse time by warp apply, but the resulting error spam is voluminous — starting from a known-good shape is faster.

1b: Show the Parsed Changes to the User

Surface the requested-changes summary as a table:

Ticket AW-123 requests:
  - <chain> / tokenFee.feeContracts.<chain>.bps : 5 → 10
  - <chain> / interchainSecurityModule.modules[1].maxCapacity (rateLimitedIsm) : 5e24 → 2e25
  - <chain> / owner : 0xABC… → 0xDEF…

Ask the user to correct anything wrong before proceeding. No [CONFIRM:] here — this is information gathering, not destructive.


Step 2: Detect the Source of Truth (YAML or Config Getter)

Warp routes come in two flavors in this monorepo, and the edit path branches on which one:

  • Plain YAML route — the deployments/warp_routes/<TOKEN>/<chains>-deploy.yaml in the registry IS the source of truth. Edit it directly.
  • Config-getter route — the source of truth is a TypeScript getter in typescript/infra/config/environments/<env>/warp/configGetters/, registered in warpConfigGetterMap in typescript/infra/config/warp.ts. The registry's deploy.yaml is GENERATED from the getter via typescript/infra/scripts/warp-routes/export-warp-configs.ts. Editing the registry YAML directly will be silently overwritten the next time the export script runs.

Determine which kind the route is by checking the map:

grep -E "^\s*'?<WARP_ROUTE_ID>'?:" typescript/infra/config/warp.ts

If the route ID is keyed in warpConfigGetterMap (or in the per-environment dispatch in that file), it's a getter-backed route. Note which getter file it points to (e.g. getRenzoPZETHWarpConfig.ts). If the route ID isn't in the map at all, it's a plain YAML route.

Then fetch the CURRENT state (regardless of source flavor):

REGISTRY_PATH="${HYPERLANE_REGISTRY:-$(pwd)/../hyperlane-registry}"
cat $REGISTRY_PATH/deployments/warp_routes/<TOKEN>/<chains-alphabetical>-deploy.yaml

Show the user the current YAML content + the flavor (YAML / getter-backed + which getter file). This is the baseline the warp-apply diff is computed against either way; the difference shows up in Step 3 (where the edit lands).


Step 3: Apply Edits

For each requested change from Step 1, target the precise field. Parse the YAML, mutate the target, serialize back. Do NOT do a global string replace — that's how field-targeting bugs sneak in.

Preserve the existing YAML formatting (alphabetical chain order at the top level AND alphabetical keys within each chain entry, per /registry-yaml-sort-policy). The registry CI / CodeRabbit enforces both invariants.

3a: Plain YAML Route — Edit the Registry File Directly

If Step 2 classified the route as plain YAML, edit $REGISTRY_PATH/deployments/warp_routes/<TOKEN>/<chains>-deploy.yaml in place. Show the user the diff (unified format, - old, + new). End your message with:

[CONFIRM: Apply deploy.yaml edits for <ticket-id>]

If confirmed, write the updated deploy.yaml back to the registry.

3b: Config-Getter Route — Edit the Getter, Then Regenerate

If Step 2 classified the route as getter-backed:

  1. Edit the getter TS file in typescript/infra/config/environments/<env>/warp/configGetters/<getter>.ts to encode the requested changes. The getter returns a ChainMap<HypTokenRouterConfig>; mutate the relevant fields in the returned object. Show the user the unified TS diff.

  2. Regenerate the registry YAML by running the export script for ONLY this route:

    pnpm -C typescript/infra tsx scripts/warp-routes/export-warp-configs.ts \
      -e <env> \
      --warpRouteIds <WARP_ROUTE_ID>
    

    This calls the getter, sorts the output per WARP_YAML_SORT_CONFIG, and writes $REGISTRY_PATH/deployments/warp_routes/<TOKEN>/<chains>-deploy.yaml in place.

  3. Diff the regenerated YAML against the previous version (e.g. git -C $REGISTRY_PATH diff deployments/warp_routes/<TOKEN>/). The diff should match the conceptual change requested in the ticket; if other fields drifted, the getter has a bug or the export script picked up unrelated env state. Halt and surface the unexpected diff to the user before continuing.

End your message with:

[CONFIRM: Apply getter edits + regenerated deploy.yaml for <ticket-id>]

Note: [CONFIRM: ...] is a Haggis-specific harness primitive — Haggis renders it as an inline approve/reject button. In other Claude Code contexts it is just text.

For getter-backed routes, both changes ship as PRs in Step 11:

  • Monorepo PR for the getter edit (no changeset needed — @hyperlane-xyz/infra is private: true).
  • Registry PR for the regenerated YAML + changeset (per the standard Step 11 flow).

Step 11 covers the registry PR; for the monorepo PR open it separately with a normal gh pr create against main, scoping git add to the specific getter file + typescript/infra/config/warp.ts if you touched the map.


Step 4: Build the Strategy File

4a: Map Owner Types to Submitters

The strategy file tells warp apply how to submit each chain's transactions. For each chain in the route, look up the artifact owners from the auto-loaded artifact context (~/.hyperlane/update-context/<ticket-id>.yaml) and map the owner type — AND the controller type of any wrapping account — to the right submitter:

Owner type from artifact contextStrategy submitter
SafegnosisSafeTxBuilder with that chain's Safe address
ICA (controller = Safe)interchainAccount with origin: <controllingChain>, owner: <controllerAddress>, internalSubmitter: gnosisSafeTxBuilder targeting the controlling Safe
ICA (controller = EOA)interchainAccount with origin: <controllingChain>, owner: <controllerEOAaddress>, internalSubmitter: file (write the callRemote for the EOA to broadcast later) or internalSubmitter: jsonRpc (execute live with the EOA's key)
Squadsfile (no native Squads submitter in warp apply; the propose skill calls submitProposalToSquads against the file later)
EOAjsonRpc if it's the deployer key signing post-deploy ops; otherwise halt — EOAs should have been caught at /warp-deploy-validate-owners and rejected as production owners
Turnkey / Privy / MPC / CustomMultisig / Timelock / Otherfile (manual hand-off to the relevant tooling)
UnknownHalt — can't dispatch without an owner classification

The ICA branch has two sub-cases, not one. Don't unconsciously default to internalSubmitter: gnosisSafeTxBuilder just because most production ICAs are Safe-controlled. Read the controller type from the artifact context (/warp-update-resolve-artifacts Step 3 walks each ICA's InterchainAccountRouter to resolve the controller and classifies it). If the controller is an EOA, the Safe-builder path will silently produce nonsensical output that no Safe app can consume — pick file or jsonRpc instead.

If different artifacts on the same chain have different owners (router → AW Safe vs. fee contracts → Foundation Safe is the common case), the strategy needs a feeSubmitter block in addition to the main submitter block — see /warp-update-extend Step 7's "fee contract with a separate fee Safe" pattern.

Write the strategy file to ~/.hyperlane/strategies/<ticket-id>-strategy.yaml. Show the user the strategy.

4b: Preview Expected Output Files Before Confirming

Before the user confirms the strategy, describe what warp apply will write to the receipts directory — per chain, per submitter type. This is the structural guardrail: catching a wrong-strategy-shape (raw owner-ops vs. ICA-wrapped, Safe-builder JSON vs. SVM versioned-tx array) BEFORE the apply phase deploys mainnet contracts means the user can reject the strategy here, not after seeing wrong-format files post-deploy.

For each chain entry in the strategy, surface:

  1. Expected filename pattern in the receipts directory
  2. The shape of the JSON / file body
  3. Who downstream consumes that file (so the user can sanity-check the strategy serves the intended consumer)

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
75
Forks
601
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
warp-update
Source
github.com/hyperlane-xyz/hyperlane-monorepo