Move KMS signing from Foundry to Hardhat
This guide moves KMS signing from Foundry to Hardhat: the environment variables Foundry reads pick the key, from the command line as in Foundry, or from the config.
Foundry picks a KMS signer per command with --aws or --gcp, and reads the key from environment variables. hardhat-kms reads the same variables, in two ways. Both need the provider's package in plugins: @hardhat-kms/aws (Set up an AWS KMS key), @hardhat-kms/gcp (Set up a Google Cloud KMS key) or @hardhat-kms/azure (Set up an Azure Key Vault key).
NOTE
Audience: Foundry users moving KMS signing to Hardhat.
--kms keys are added to the selected network and sign messages, typed data and transactions, with keys in AWS KMS, Google Cloud KMS or Azure Key Vault.
From the command line, as in Foundry
Add --kms with the providers to load:
# Foundry
AWS_KMS_KEY_ID=alias/deployer forge script script/Deploy.s.sol --rpc-url "$SEPOLIA_RPC_URL" --aws --broadcast
# Hardhat
AWS_KMS_KEY_ID=alias/deployer npx hardhat run scripts/deploy.ts --network sepolia --kms aws# Foundry
AWS_KMS_KEY_ID=alias/deployer forge script script/Deploy.s.sol --rpc-url "$SEPOLIA_RPC_URL" --aws --broadcast
# Hardhat
AWS_KMS_KEY_ID=alias/deployer pnpm hardhat run scripts/deploy.ts --network sepolia --kms aws# Foundry
AWS_KMS_KEY_ID=alias/deployer forge script script/Deploy.s.sol --rpc-url "$SEPOLIA_RPC_URL" --aws --broadcast
# Hardhat
AWS_KMS_KEY_ID=alias/deployer yarn hardhat run scripts/deploy.ts --network sepolia --kms awsOn AWS, use one credential source before you run these: unset either AWS_PROFILE, or AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN. When both are set, Foundry signs with the access keys and this plugin with the profile (Credentials). The two can be different identities, and the same alias can then name a different key in each. Before you deploy, check that npx hardhat --kms aws kms address AWS_KMS_KEY_ID prints the same address as cast wallet address --aws.
--kms value | Variables read, as in Foundry |
|---|---|
aws | AWS_KMS_KEY_IDS (comma-separated) if set, else AWS_KMS_KEY_ID |
gcp | GCP_PROJECT_ID, GCP_LOCATION, GCP_KEY_RING, GCP_KEY_NAME, GCP_KEY_VERSION |
azure | AZURE_KEY_VAULT_KEY_IDS (comma-separated) if set, else AZURE_KEY_VAULT_KEY_ID |
Foundry has no Azure Key Vault signer in a release yet: Foundry 1.8.4, the latest on 2026-10-03, has no --azure. The signer is proposed in foundry-rs/foundry#17120, a draft that was still open on that date. The azure variable names follow that proposal and may change before it ships.
Several providers:
--kms aws,azure.In CI,
HARDHAT_KMS=awsdoes the same as the option. Hardhat reads it for every command, so do not leave it in a shell profile unless every Hardhat command there should load KMS keys.Nothing is read unless
--kmsnames the provider.As in Foundry, the single variable can also hold a comma-separated list. An empty variable counts as unset, and
HARDHAT_KMS=turns the option off.npx hardhat --helpworks even whenHARDHAT_KMSis wrong.Hardhat reads and checks the variables when it starts, so a missing or malformed one fails before any task runs. The error names the variable and never shows its value:
textError in community plugin hardhat-kms: --kms gcp: GCP_PROJECT_ID is not setThe keys sign on the
--networkyou select, after any keys the config gives that network.cast wallet address --awsbecomesnpx hardhat --kms aws kms address AWS_KMS_KEY_ID: a task names a--kmskey by its variable (tasks reference). For--kms gcpthe name isGCP_KEY_*, notGCP_KEY_NAME. Quote it, since zsh refuses an unquoted*that matches no file:npx hardhat --kms gcp kms address 'GCP_KEY_*'.cast wallet sign --aws <message>becomesnpx hardhat --kms aws kms sign AWS_KMS_KEY_ID <message>, with the same--data,--from-fileand--no-hashflags and the same output. Unlike cast, which also accepts 64 hex digits without a prefix,--no-hashneeds the0xprefix. Typed data that names a chain also needs--network,--chainor--allow-cross-chain, which cast does not check (kms sign).cast wallet verify --address <address> <message> <signature>becomesnpx hardhat kms verify --address <address> <message> <signature>, with the same--data [--from-file]flags.--key <key>checks against a KMS key's address instead. There is no--no-hash(kms verify).cast wallet list --awsbecomesnpx hardhat --kms aws kms accounts. It also lists the keys in the config and checks eachaddresspin. A key that fails is shown next to its name and makes the command exit with code 1, wherecast wallet listprints the error and exits 0 (kms accounts).cast mktx --aws --rpc-url <url> <to> --value 1becomesnpx hardhat --kms aws --network <network> kms sign-tx AWS_KMS_KEY_ID tx.json, with{ "to": "<to>", "value": "0x1" }intx.json. As with cast, standard output holds only the raw transaction, socast publish $(npx hardhat ... kms sign-tx ...)sends it; the hash goes to standard error. The task reads the transaction from a JSON file witheth_sendTransactionfields, so calldata goes indataalready encoded, wherecast mktxtakes a function signature and arguments (kms sign-tx).cast wallet sign-auth --aws <address>becomesnpx hardhat --kms aws kms sign-auth --network <name> AWS_KMS_KEY_ID <address>, with the same--nonce,--chainand--self-broadcastflags. cast prints the RLP-encoded authorization; the task prints the JSON tuple that Hardhat'sauthorizationListtakes.--chaintakes numeric chain ids only, not names such asmainnet. Pass--chainor--network, not both: without--network, also pass--nonce. Chain 0 needs--force, which cast does not ask for (kms sign-auth).
In the config
To keep the choice in hardhat.config.ts, point each key at the same variables with configVariable():
import hardhatKmsAws from "@hardhat-kms/aws";
import { configVariable, defineConfig } from "hardhat/config";
export default defineConfig({
plugins: [hardhatKmsAws],
kms: {
keys: {
deployer: { provider: "aws", keyId: configVariable("AWS_KMS_KEY_ID") },
},
},
networks: {
sepolia: {
type: "http",
url: configVariable("SEPOLIA_RPC_URL"),
chainId: 11155111,
kmsAccounts: ["deployer"],
},
},
});The configuration reference covers every key form. On AWS and Google Cloud, credentials come from the SDK's default chain in both cases, as in Foundry, with one difference on AWS: today, when AWS_PROFILE or a key's profile is set, the AWS SDK for JavaScript ignores AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, where Foundry uses them. The SDK's own warning says a future version may prefer the keys. Never set both: a CI job that exports access keys must not also set a profile (Credentials). Azure uses the plugin's own chain, since no Foundry release has an Azure signer yet (Credentials). Comparison with Foundry puts the chains side by side.