Hardhat Projects skill for Codex
Config, deploy and verify without leaking a key. Codex detects whether a repo is on Hardhat 2 or 3 and keeps to that version's idioms, moves secrets into configVariable and the encrypted keystore, writes tests with viem and node:test, and deploys through resumable Ignition modules with parameter files. Verification uses the same build profile as the deploy, and mainnet deploys wait for your confirmation.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- hardhat-projects Installs to ~/.agents/skills/hardhat-projects/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install hardhat-projects 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 hardhat-projects Installs to .agents/skills/hardhat-projects/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/hardhat-projects.zipunzip -o hardhat-projects.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))) hardhat-projects 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 $hardhat-projects, 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
Sets up and maintains Hardhat 3 Solidity projects (and legacy Hardhat 2) - defineConfig with build profiles and networks, configVariable and the keystore for secrets, Solidity and node:test tests with viem, Hardhat Ignition deployment modules, and contract verification on Etherscan, Blockscout and Sourcify. Use when asked to scaffold a Hardhat project, deploy to a testnet or mainnet, verify a contract, migrate from Hardhat 2 or ethers scripts, or fix a broken hardhat.config.ts.
Use it when
- You are starting or maintaining a Hardhat-based Solidity repo
- A contract needs deploying to a testnet or mainnet with reproducible records
- Etherscan, Blockscout or Sourcify verification keeps failing
- Secrets are sitting in hardhat.config or a committed .env
Use something else when
- Tests are written in Solidity with forge and need fuzzing or invariants: use foundry-testing
- You need to plan a proxy upgrade rather than a fresh deploy: use upgradeable-contracts
How to check the result
npx hardhat test passes on a clean clone, Ignition deployment state is committed, and the explorer shows the contract as verified with matching constructor arguments.
The mistake it prevents
Building the deploy with the default profile and verifying with the production profile, so the bytecode never matches and verification fails.
A Hardhat project is in good shape when secrets never sit in the config or .env that gets
committed, deployments are declarative and resumable (Ignition), the verify step uses the
same build profile as the deploy, and anyone can run npx hardhat test on a fresh clone.
Inspect first
- Read
AGENTS.md,package.jsonandhardhat.config.*. Thehardhatversion decides everything below: 3.x usesdefineConfig, ESM and plugins arrays; 2.x usesmodule.exports/HardhatUserConfigandrequireside-effect plugins. - Note the package manager from the lockfile and use it (
npm,pnpm,yarn,bun). - Hardhat 3 needs Node.js 22+ and
"type": "module"inpackage.json. Checknode -v. - Run
npx hardhat build(3.x) ornpx hardhat compile(2.x) andnpx hardhat testto get a baseline before changing anything.
Installing packages needs network; in a Codex sandbox ask for approval first.
Version decision
| Situation | Do |
|---|---|
| New project | Hardhat 3: npx hardhat --init, choose the viem + node:test template |
| Existing Hardhat 2 project, working | Stay on 2.x unless the user asks to migrate; do not mix idioms |
| Need fast Solidity unit tests | Hardhat 3 Solidity tests (forge-std compatible) or Foundry alongside |
| Repo already uses Foundry for tests | Keep Foundry for tests; Hardhat only if deployment tooling needs it |
Hardhat 3 config
// hardhat.config.ts
import { configVariable, defineConfig } from "hardhat/config";
import hardhatToolboxViem from "@nomicfoundation/hardhat-toolbox-viem";
export default defineConfig({
plugins: [hardhatToolboxViem],
solidity: {
profiles: {
default: { version: "0.8.28" },
production: {
version: "0.8.28",
settings: { optimizer: { enabled: true, runs: 200 } },
},
},
},
networks: {
hardhatMainnet: { type: "edr-simulated", chainType: "l1" },
hardhatOp: { type: "edr-simulated", chainType: "op" },
sepolia: {
type: "http",
chainType: "l1",
url: configVariable("SEPOLIA_RPC_URL"),
accounts: [configVariable("SEPOLIA_PRIVATE_KEY")],
},
},
verify: {
etherscan: { apiKey: configVariable("ETHERSCAN_API_KEY") },
},
});
configVariable("X")is resolved lazily, only when a task needs it, from environment variables or the encrypted keystore. A missing variable fails the task that uses it, not every task.- Store secrets in the keystore, never in the file:
npx hardhat keystore set SEPOLIA_PRIVATE_KEY(prompts for a password; the user must type the secret themselves, so ask them to run it). chainType: "op"makes the simulated network behave like OP Stack chains (gas, L1 fees).- Plugins are listed in
plugins; importing a plugin file for its side effects (the 2.x way) does nothing in 3.x.
Tests
Hardhat 3 runs Solidity tests and TypeScript tests from one command:
npx hardhat test # everything
npx hardhat test solidity # *.t.sol files, forge-std style
npx hardhat test nodejs # node:test files in test/
TypeScript test with viem:
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { network } from "hardhat";
describe("Vault", async () => {
const { viem } = await network.connect();
const [owner, alice] = await viem.getWalletClients();
it("records deposits", async () => {
const token = await viem.deployContract("ERC20Mock");
const vault = await viem.deployContract("Vault", [token.address, owner.account.address]);
await token.write.mint([alice.account.address, 100n]);
await token.write.approve([vault.address, 100n], { account: alice.account });
await vault.write.deposit([100n], { account: alice.account });
assert.equal(await vault.read.balanceOf([alice.account.address]), 100n);
});
});
- Each
network.connect()returns a fresh, isolated simulated chain; connect once perdescribeand use fixtures for repeated setup. - Pass
network.connect("hardhatOp")to test against OP-style gas accounting. - For deep fuzzing and invariants, Foundry is stronger; see
$foundry-testing.
Deploy with Ignition
// ignition/modules/Vault.ts
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
export default buildModule("VaultModule", (m) => {
const owner = m.getParameter("owner");
const token = m.getParameter("token");
const vault = m.contract("Vault", [token, owner]);
return { vault };
});
// ignition/parameters/sepolia.json
{ "VaultModule": { "owner": "0xYourSafe...", "token": "0xToken..." } }
npx hardhat ignition deploy ignition/modules/Vault.ts \
--network sepolia --parameters ignition/parameters/sepolia.json
- Ignition records state under
ignition/deployments/chain-<id>/. Re-running the same command resumes a partial deployment instead of redeploying. Commit that folder for production deployments; it is the source of truth for addresses. - Deploy to the simulated network first (omit
--network), then a testnet, then mainnet. - Mainnet or any production chain: show the user the module, parameters, deployer address and estimated cost, and get explicit confirmation before running the deploy. Ownership should go to a multisig, not the deployer EOA.
Verify
npx hardhat build --build-profile production
npx hardhat verify --network sepolia 0xDeployed "0xToken..." "0xOwner..."
npx hardhat verify blockscout --network sepolia 0xDeployed "0xToken..." "0xOwner..."
verifyuses theproductionbuild profile by default. If the deploy was built withdefault, bytecode will not match: build and deploy with the same profile you verify.- Sourcify verification runs without an API key. Etherscan uses one key for all supported
chains (the v2 API); chains not built in need a
chainDescriptorsentry. - Constructor arguments must be passed exactly as deployed, in order. Complex arguments
(structs, arrays) are easier through the programmatic
verifyContracthelper from@nomicfoundation/hardhat-verify/verify.
Hardhat 2 (legacy) equivalents
| Hardhat 3 | Hardhat 2 |
|---|---|
defineConfig({ plugins: [...] }) | require("@nomicfoundation/hardhat-toolbox") |
configVariable("KEY") + keystore | vars.get("KEY") with npx hardhat vars set KEY |
npx hardhat build | npx hardhat compile |
network.connect() per test file | Global hre.network, loadFixture from network-helpers |
verify.etherscan.apiKey | etherscan: { apiKey } (needs a recent hardhat-verify for the v2 API) |
Chai matchers in 2.x (ethers v6): await expect(tx).to.be.revertedWithCustomError(vault, "ZeroAmount") and .to.emit(vault, "Deposited").withArgs(alice.address, 100n).
Migrating 2 to 3
- [ ] 1. Branch; record current test results and deployed addresses
- [ ] 2. Node 22+, "type": "module", rename config to hardhat.config.ts with defineConfig
- [ ] 3. Replace side-effect plugin imports with the plugins array (toolbox-viem or mocha-ethers)
- [ ] 4. Move secrets from .env / vars to configVariable + keystore
- [ ] 5. Port tests: network.connect() at the top of each file; fix ESM imports
- [ ] 6. Port deploy scripts to Ignition modules; keep old deployment records
- [ ] 7. Run the full suite; diff compiled bytecode for unchanged contracts
Gotchas
- Hard-coded private keys in
hardhat.config.*or a committed.envare a leak. Checkgit log -p -- hardhat.config.ts .envif one was ever there and tell the user to rotate. biginteverywhere in viem:100n, not100.parseEther("1")for ETH amounts.- Gas estimation on a fork of an L2 can differ from the real chain; leave headroom.
- Contract names that exist in two files need fully qualified names:
viem.deployContract("contracts/a/Vault.sol:Vault"). - Do not run
npx hardhat cleanin the middle of an Ignition deployment you intend to resume without checking the build-info it relies on.
Report
**Hardhat:** 3.x, toolbox-viem, solc 0.8.28 (production profile: optimizer 200 runs)
**Tests:** 42 passing (31 Solidity, 11 node:test)
**Deployment:** VaultModule on sepolia, Vault at 0x..., state in ignition/deployments/chain-11155111
**Verification:** Etherscan and Sourcify verified (links)
**Secrets:** SEPOLIA_PRIVATE_KEY, ETHERSCAN_API_KEY in keystore; none in repo 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: "Hardhat Projects"
short_description: "Config, deploy and verify without leaking a key"
default_prompt: "$hardhat-projects Set up a Hardhat 3 project with viem tests and a Sepolia network" 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.