
Monskill
- 407 installs
- 11 repo stars
- Updated July 30, 2026
- therealharpaljadeja/monskills
Helps with ai & agent building tasks.
About
monskill is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- monskill
- AI & Agent Building
- AI-coding skill
Monskill by the numbers
- 407 all-time installs (skills.sh)
- +46 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,957 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/therealharpaljadeja/monskills --skill monskillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 407 |
|---|---|
| repo stars | ★ 11 |
| Last updated | July 30, 2026 |
| Repository | therealharpaljadeja/monskills ↗ |
What it does
Helps with ai & agent building tasks.
Files
CRITICAL
⚠️ Always verify addresses using the explorer before interacting with smart contracts. Never hallucinate a smart contract address, wrong addresses can lead to loss of funds.
| Network | Explorer |
|---|---|
| Monad Mainnet | monadscan.com |
| Monad Testnet | testnet.monadscan.com |
Make sure you have zero doubts about which network the user is asking the address for whether mainnet or testnet, if you are unsure then ask the user. Do not provide mainnet address when a testnet address was asked for.
How to verify if a smart contract has code on a network.
If Foundry toolkit is installed.
Monad Mainnet
# Check bytecode exists
cast code [smart_contract_address] --rpc-url https://rpc.monad.xyzMonad Testnet
# Check bytecode exists
cast code [smart_contract_address] --rpc-url https://testnet-rpc.monad.xyzIf Foundry toolkit is not installed you can call "eth_getCode" RPC method on the respective RPC endpoint for the network with the smart contract address and verify using the response.
Canonical contracts (on Monad mainnet)
| Name | Address |
|---|---|
| Wrapped MON | 0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A |
| Create2Deployer | 0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2 |
| CreateX | 0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed |
| ERC-2470 Singleton Factory | 0xce0042b868300000d44a59004da54a005ffdcf9f |
| ERC-4337 EntryPoint v0.6 | 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 |
| ERC-4337 SenderCreator v0.6 | 0x7fc98430eAEdbb6070B35B39D798725049088348 |
| ERC-4337 EntryPoint v0.7 | 0x0000000071727De22E5E9d8BAf0edAc6f37da032 |
| ERC-4337 SenderCreator v0.7 | 0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C |
| ERC-6492 UniversalSigValidator | 0xdAcD51A54883eb67D95FAEb2BBfdC4a9a6BD2a3B |
| Foundry Deterministic Deployer | 0x4e59b44847b379578588920ca78fbf26c0b4956c |
| Multicall3 | 0xcA11bde05977b3631167028862bE2a173976CA11 |
| MultiSend | 0x998739BFdAAdde7C933B942a68053933098f9EDa |
| MultiSendCallOnly | 0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B |
| Permit2 | 0x000000000022d473030f116ddee9f6b43ac78ba3 |
| Safe | 0x69f4D1788e39c87893C980c06EdF4b7f686e2938 |
| SafeL2 | 0xfb1bffC9d739B8D520DaF37dF666da4C687191EA |
| SafeSingletonFactory | 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7 |
| SimpleAccount | 0x68641DE71cfEa5a5d0D29712449Ee254bb1400C2 |
| Simple7702Account | 0xe6Cae83BdE06E4c305530e199D7217f42808555B |
| Sub Zero VanityMarket | 0x000000000000b361194cfe6312EE3210d53C15AA |
| Zoltu Deterministic Deployment Proxy | 0x7A0D94F55792C434d74a40883C6ed8545E406D12 |
AI & Agent Standards
ERC-8004 (same addresses for Monad mainnet and testnet)
| Contract | Address |
|---|---|
| IdentityRegistry | 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 |
| ReputationRegistry | 0x8004BAa17C55a88189AE136b182e5fdA19dE9b63 |
Bridged Assets Dollar-related (on Monad mainnet)
| Symbol | Name | Address |
|---|---|---|
| AUSD | Agora USD | 0x00000000eFE302BEAA2b3e6e1b18d08D69a9012a |
| USDC | USD Coin | 0x754704Bc059F8C67012fEd69BC8A327a5aafb603 |
| USDT0 | Tether USD | 0xe7cd86e13AC4309349F30B3435a9d337750fC82D |
| USD1 | USD1 | 0x111111d2bf19e43C34263401e0CAd979eD1cdb61 |
| thBILL | Theo Short Duration UST Fund | 0xfDD22Ce6D1F66bc0Ec89b20BF16CcB6670F55A5a |
| wsrUSD | Wrapped srUSD | 0x4809010926aec940b550D34a46A52739f996D75D |
| yzUSD | Yuzu USD | 0x9dcB0D17eDDE04D27F387c89fECb78654C373858 |
| syzUSD | Staked Yuzu USD | 0x484be0540aD49f351eaa04eeB35dF0f937D4E73f |
Bridged Assets ETH-related (on Monad mainnet)
| Symbol | Name | Address |
|---|---|---|
| WETH | Wrapped Ether | 0xEE8c0E9f1BFFb4Eb878d8f15f368A02a35481242 |
| ezETH | Renzo Restaked ETH | 0x2416092f143378750bb29b79eD961ab195CcEea5 |
| wstETH | Lido Wrapped Staked ETH | 0x10Aeaf63194db8d453d4D85a06E5eFE1dd0b5417 |
| weETH | Wrapped EtherFi ETH | 0xA3D68b74bF0528fdD07263c60d6488749044914b |
| pufETH | pufETH | 0x37D6382B6889cCeF8d6871A8b60E667115eDDBcF |
Bridged Assets BTC-related (on Monad mainnet)
| Symbol | Name | Address |
|---|---|---|
| cbBTC | Coinbase Wrapped BTC | 0xd18B7EC58Cdf4876f6AFebd3Ed1730e4Ce10414b |
| WBTC | Wrapped Bitcoin | 0x0555E30da8f98308EdB960aa94C0Db47230d2B9c |
| LBTC | Lombard Staked Bitcoin | 0xecAc9C5F704e954931349Da37F60E39f515c11c1 |
| BTC.b | BTC.b | 0xB0F70C0bD6FD87dbEb7C10dC692a2a6106817072 |
| SolvBTC | Solv BTC | 0xaE4EFbc7736f963982aACb17EFA37fCBAb924cB3 |
| xSolvBTC | xSolvBTC | 0xc99F5c922DAE05B6e2ff83463ce705eF7C91F077 |
Bridged Assets - Others (on Monad mainnet)
| Symbol | Name | Address |
|---|---|---|
| WSOL | Wrapped SOL | 0xea17E5a9efEBf1477dB45082d67010E2245217f1 |
| XAUt0 | Tether Gold | 0x01bFF41798a0BcF287b996046Ca68b395DbC1071 |
Natively-Issued Assets (on Monad mainnet)
| Symbol | Name | Address |
|---|---|---|
| WMON | Wrapped MON | 0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A |
| mEDGE | Midas mEDGE | 0x1c8eE940B654bFCeD403f2A44C1603d5be0F50Fa |
MON on other blockchains
| Name | Blockchain | Address |
|---|---|---|
| WMON | Solana | CrAr4RRJMBVwRsZtT62pEhfA9H5utymC2mVx8e7FreP2 |
| WMON | Ethereum | 0x6917037f8944201b2648198a89906edf863b9517 |
Protocols repo
The protocols repo contains smart contract address (testnet and mainnet) for well known protocols and projects in the monad ecosystem.
If you need an address that is not found from the tables above check the protocols repo for the smart contract address.
Protocol repo GitHub URL: https://github.com/monad-crypto/protocols
Addresses in protocols repo are organized by testnet and mainnet and each protocol has it's own file.
For example:
Smart contract addresses for Clober protocol on Monad mainnet is in file: protocols/mainnet/clober.jsonc
Each file has a JSON object with property "addresses" which is object with items as "name" and address of each smart contract associated with the respected protocol.
Similarly for testnet, the route is protocols/testnet/[protocol_name].json
Token list repo
The token list repo contains smart contract address (mainnet only) for well known tokens in the monad ecosystem.
If you need an address for a token that is not found from the tables above check the token list repo for smart contract address.
Token list repo GitHub URL: https://github.com/monad-crypto/token-list
Addresses in token list repo are organized by token names in folders, there is a mainnet folder inside which there are folders with token names inside each folder is a json with the address.
CRITICAL
⚠️ After looking at all the available options above, if you are still not able to find the address ask the user for it but do not at all hallucinate an address and do not provide mainnet address when a testnet address was asked for.
{
"name": "monskills",
"owner": {
"name": "Harpalsinh Jadeja"
},
"metadata": {
"version": "0.7.0",
"description": "Skills for developing apps on Monad. Assists LLM with frontend, smart contract deployment, and deploying to production"
},
"plugins": [
{
"name": "monskills",
"source": "./",
"version": "0.7.0",
"description": "Skills for developing apps on Monad. Assists LLM with frontend, smart contract deployment, and deploying to production",
"author": {
"name": "Harpalsinh Jadeja"
}
}
]
}
{
"name": "monskills",
"version": "0.7.0",
"description": "Skills for developing apps on Monad. Assists LLM with frontend, smart contract deployment, and deploying to production",
"author": {
"name": "Harpalsinh Jadeja",
"url": "https://github.com/therealharpaljadeja"
},
"homepage": "https://skills.devnads.com",
"repository": "https://github.com/harpalsinh/monskills",
"license": "MIT",
"keywords": ["monad", "ethereum", "solidity", "web3", "blockchain", "dapp"],
"skills": [
"./addresses",
"./wallet",
"./wallet-integration",
"./why-monad",
"./concepts",
"./scaffold",
"./gas",
"./tooling-and-infra",
"./feedback",
"./indexer"
]
}
DATABASE_URL=postgresql://user:password@host/dbname?sslmode=require
STATS_SECRET=your-secret-key-here
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
name: CodeQL
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: javascript
- uses: github/codeql-action/analyze@v3
.DS_Store
node_modules/
.env
.env.local
重要提示
⚠️ 在与智能合约交互之前,务必通过区块浏览器验证地址。绝不要凭空捏造智能合约地址,错误的地址可能导致资金损失。
| 网络 | 区块浏览器 |
|---|---|
| Monad 主网 | monadscan.com |
| Monad 测试网 | testnet.monadscan.com |
请确保完全明确用户询问的是主网还是测试网的地址,如果不确定请向用户确认。不要在用户询问测试网地址时提供主网地址。
如何验证智能合约在某个网络上是否有代码
如果已安装 Foundry 工具包:
Monad 主网
# Check bytecode exists
cast code [smart_contract_address] --rpc-url https://rpc.monad.xyzMonad 测试网
# Check bytecode exists
cast code [smart_contract_address] --rpc-url https://testnet-rpc.monad.xyz如果未安装 Foundry 工具包,你可以在相应网络的 RPC 端点上调用 eth_getCode RPC 方法,传入智能合约地址并通过返回结果进行验证。
规范合约(Monad 主网)
| 名称 | 地址 |
|---|---|
| Wrapped MON | 0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A |
| Create2Deployer | 0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2 |
| CreateX | 0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed |
| ERC-2470 Singleton Factory | 0xce0042b868300000d44a59004da54a005ffdcf9f |
| ERC-4337 EntryPoint v0.6 | 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 |
| ERC-4337 SenderCreator v0.6 | 0x7fc98430eAEdbb6070B35B39D798725049088348 |
| ERC-4337 EntryPoint v0.7 | 0x0000000071727De22E5E9d8BAf0edAc6f37da032 |
| ERC-4337 SenderCreator v0.7 | 0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C |
| ERC-6492 UniversalSigValidator | 0xdAcD51A54883eb67D95FAEb2BBfdC4a9a6BD2a3B |
| Foundry Deterministic Deployer | 0x4e59b44847b379578588920ca78fbf26c0b4956c |
| Multicall3 | 0xcA11bde05977b3631167028862bE2a173976CA11 |
| MultiSend | 0x998739BFdAAdde7C933B942a68053933098f9EDa |
| MultiSendCallOnly | 0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B |
| Permit2 | 0x000000000022d473030f116ddee9f6b43ac78ba3 |
| Safe | 0x69f4D1788e39c87893C980c06EdF4b7f686e2938 |
| SafeL2 | 0xfb1bffC9d739B8D520DaF37dF666da4C687191EA |
| SafeSingletonFactory | 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7 |
| SimpleAccount | 0x68641DE71cfEa5a5d0D29712449Ee254bb1400C2 |
| Simple7702Account | 0xe6Cae83BdE06E4c305530e199D7217f42808555B |
| Sub Zero VanityMarket | 0x000000000000b361194cfe6312EE3210d53C15AA |
| Zoltu Deterministic Deployment Proxy | 0x7A0D94F55792C434d74a40883C6ed8545E406D12 |
AI 与代理标准
ERC-8004(Monad 主网和测试网地址相同)
| 合约 | 地址 |
|---|---|
| IdentityRegistry | 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 |
| ReputationRegistry | 0x8004BAa17C55a88189AE136b182e5fdA19dE9b63 |
桥接资产 — 美元相关(Monad 主网)
| 代币符号 | 名称 | 地址 |
|---|---|---|
| AUSD | Agora USD | 0x00000000eFE302BEAA2b3e6e1b18d08D69a9012a |
| USDC | USD Coin | 0x754704Bc059F8C67012fEd69BC8A327a5aafb603 |
| USDT0 | Tether USD | 0xe7cd86e13AC4309349F30B3435a9d337750fC82D |
| USD1 | USD1 | 0x111111d2bf19e43C34263401e0CAd979eD1cdb61 |
| thBILL | Theo Short Duration UST Fund | 0xfDD22Ce6D1F66bc0Ec89b20BF16CcB6670F55A5a |
| wsrUSD | Wrapped srUSD | 0x4809010926aec940b550D34a46A52739f996D75D |
| yzUSD | Yuzu USD | 0x9dcB0D17eDDE04D27F387c89fECb78654C373858 |
| syzUSD | Staked Yuzu USD | 0x484be0540aD49f351eaa04eeB35dF0f937D4E73f |
桥接资产 — ETH 相关(Monad 主网)
| 代币符号 | 名称 | 地址 |
|---|---|---|
| WETH | Wrapped Ether | 0xEE8c0E9f1BFFb4Eb878d8f15f368A02a35481242 |
| ezETH | Renzo Restaked ETH | 0x2416092f143378750bb29b79eD961ab195CcEea5 |
| wstETH | Lido Wrapped Staked ETH | 0x10Aeaf63194db8d453d4D85a06E5eFE1dd0b5417 |
| weETH | Wrapped EtherFi ETH | 0xA3D68b74bF0528fdD07263c60d6488749044914b |
| pufETH | pufETH | 0x37D6382B6889cCeF8d6871A8b60E667115eDDBcF |
桥接资产 — BTC 相关(Monad 主网)
| 代币符号 | 名称 | 地址 |
|---|---|---|
| cbBTC | Coinbase Wrapped BTC | 0xd18B7EC58Cdf4876f6AFebd3Ed1730e4Ce10414b |
| WBTC | Wrapped Bitcoin | 0x0555E30da8f98308EdB960aa94C0Db47230d2B9c |
| LBTC | Lombard Staked Bitcoin | 0xecAc9C5F704e954931349Da37F60E39f515c11c1 |
| BTC.b | BTC.b | 0xB0F70C0bD6FD87dbEb7C10dC692a2a6106817072 |
| SolvBTC | Solv BTC | 0xaE4EFbc7736f963982aACb17EFA37fCBAb924cB3 |
| xSolvBTC | xSolvBTC | 0xc99F5c922DAE05B6e2ff83463ce705eF7C91F077 |
桥接资产 — 其他(Monad 主网)
| 代币符号 | 名称 | 地址 |
|---|---|---|
| WSOL | Wrapped SOL | 0xea17E5a9efEBf1477dB45082d67010E2245217f1 |
| XAUt0 | Tether Gold | 0x01bFF41798a0BcF287b996046Ca68b395DbC1071 |
原生发行资产(Monad 主网)
| 代币符号 | 名称 | 地址 |
|---|---|---|
| WMON | Wrapped MON | 0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A |
| mEDGE | Midas mEDGE | 0x1c8eE940B654bFCeD403f2A44C1603d5be0F50Fa |
其他区块链上的 MON
| 名称 | 区块链 | 地址 |
|---|---|---|
| WMON | Solana | CrAr4RRJMBVwRsZtT62pEhfA9H5utymC2mVx8e7FreP2 |
| WMON | Ethereum | 0x6917037f8944201b2648198a89906edf863b9517 |
协议仓库
协议仓库包含 Monad 生态系统中知名协议和项目的智能合约地址(测试网和主网)。
如果在上方表格中找不到所需地址,请在协议仓库中查找智能合约地址。
协议仓库 GitHub 地址:https://github.com/monad-crypto/protocols
协议仓库中的地址按测试网和主网分类组织,每个协议有各自的文件。
例如:
Clober 协议在 Monad 主网上的智能合约地址位于文件:protocols/mainnet/clober.jsonc
每个文件包含一个 JSON 对象,其中 addresses 属性是一个对象,包含相应协议每个智能合约的名称和地址。
测试网的路径类似:protocols/testnet/[protocol_name].json
代币列表仓库
代币列表仓库包含 Monad 生态系统中知名代币的智能合约地址(仅限主网)。
如果在上方表格中找不到所需代币地址,请在代币列表仓库中查找智能合约地址。
代币列表仓库 GitHub 地址:https://github.com/monad-crypto/token-list
代币列表仓库中的地址按代币名称以文件夹形式组织,其中有一个 mainnet 文件夹,内部按代币名称分文件夹,每个文件夹中包含一个带有地址的 JSON 文件。
重要提示
⚠️ 查看完以上所有可用选项后,如果仍然找不到所需地址,请向用户询问,但绝对不要凭空捏造地址,也不要在用户询问测试网地址时提供主网地址。
SKILL.md
import { neon } from "@neondatabase/serverless";
import { createHash } from "crypto";
export function getDb() {
return neon(process.env.DATABASE_URL);
}
/**
* Hash IP with a daily rotating salt for anonymous unique tracking.
* Same IP on the same day = same hash (deduplication).
* Different day = different hash (can't track across days).
*/
export function hashIp(ip) {
const daySalt = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
return createHash("sha256").update(`${ip}:${daySalt}`).digest("hex");
}
import { getDb, hashIp } from "./_lib/db.js";
const MAX_MESSAGE_LEN = 5000;
const MAX_CONTEXT_LEN = 4000;
const MAX_FIELD_LEN = 128;
const ALLOWED_SOURCES = new Set(["agent", "user"]);
const ALLOWED_SEVERITY = new Set(["low", "medium", "high"]);
const ALLOWED_CATEGORIES = new Set([
"stuck",
"error-loop",
"user-complaint",
"bug",
"incorrect-info",
"suggestion",
"other",
]);
const WINDOW_SECONDS = 60 * 60;
const MAX_PER_WINDOW = 10;
function clampString(value, max) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
if (!trimmed) return null;
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
}
function looksLikeSpam(message) {
const urlMatches = message.match(/https?:\/\//gi);
if (urlMatches && urlMatches.length > 5) return true;
if (/<script[\s>]/i.test(message)) return true;
return false;
}
export default async function handler(req, res) {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (req.method === "OPTIONS") {
return res.status(204).end();
}
if (req.method !== "POST") {
return res.status(405).json({ ok: false, error: "Method not allowed" });
}
if (!process.env.DATABASE_URL) {
return res.status(500).json({ ok: false, error: "Database not configured" });
}
let body = req.body;
if (typeof body === "string") {
try {
body = JSON.parse(body);
} catch {
return res.status(400).json({ ok: false, error: "Invalid JSON body" });
}
}
if (!body || typeof body !== "object") {
return res.status(400).json({ ok: false, error: "Missing JSON body" });
}
// Honeypot: legitimate clients never fill this; bots usually do.
// Silently accept to avoid signalling that filtering exists.
if (typeof body.website === "string" && body.website.trim() !== "") {
return res.status(200).json({ ok: true });
}
const message = clampString(body.message, MAX_MESSAGE_LEN);
if (!message) {
return res.status(400).json({ ok: false, error: "message is required" });
}
if (message.length < 3) {
return res.status(400).json({ ok: false, error: "message is too short" });
}
if (looksLikeSpam(message)) {
return res.status(200).json({ ok: true });
}
const source = clampString(body.source, MAX_FIELD_LEN);
if (source && !ALLOWED_SOURCES.has(source)) {
return res.status(400).json({ ok: false, error: "invalid source" });
}
const severity = clampString(body.severity, MAX_FIELD_LEN);
if (severity && !ALLOWED_SEVERITY.has(severity)) {
return res.status(400).json({ ok: false, error: "invalid severity" });
}
const category = clampString(body.category, MAX_FIELD_LEN);
if (category && !ALLOWED_CATEGORIES.has(category)) {
return res.status(400).json({ ok: false, error: "invalid category" });
}
const skillName = clampString(body.skill || body.skill_name, MAX_FIELD_LEN);
const agentName = clampString(body.agent || body.agent_name, MAX_FIELD_LEN);
const context = clampString(body.context, MAX_CONTEXT_LEN);
const rawIp =
req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
req.headers["x-real-ip"] ||
req.socket?.remoteAddress ||
"unknown";
const ipHash = hashIp(rawIp);
const sql = getDb();
try {
const recent = await sql`
SELECT COUNT(*)::int AS count
FROM feedback
WHERE ip_hash = ${ipHash}
AND created_at > NOW() - (${WINDOW_SECONDS} || ' seconds')::interval
`;
if (recent[0]?.count >= MAX_PER_WINDOW) {
return res.status(429).json({ ok: false, error: "Too many submissions, try again later" });
}
const inserted = await sql`
INSERT INTO feedback
(source, skill_name, category, severity, message, context, agent_name, ip_hash)
VALUES
(${source || null}, ${skillName || null}, ${category || null},
${severity || null}, ${message}, ${context || null},
${agentName || null}, ${ipHash})
RETURNING id
`;
return res.status(200).json({ ok: true, id: inserted[0].id });
} catch (e) {
console.error("Failed to record feedback:", e);
return res.status(500).json({ ok: false, error: "Failed to record feedback" });
}
}
import { readFileSync } from "fs";
import { join } from "path";
import { getDb, hashIp } from "./_lib/db.js";
const VALID_SKILLS = [
"monskill",
"scaffold",
"why-monad",
"addresses",
"wallet",
"wallet-integration",
"gas",
"concepts",
"tooling-and-infra",
"feedback",
"indexer",
];
export default async function handler(req, res) {
const skill = req.query.name;
if (!skill || !VALID_SKILLS.includes(skill)) {
return res.status(404).send("Skill not found");
}
const lang = req.query.lang === "zh" ? "zh" : null;
let content;
try {
const filename = lang ? "SKILL.zh.md" : "SKILL.md";
const filePath = skill === "monskill"
? join(process.cwd(), filename)
: join(process.cwd(), skill, filename);
content = readFileSync(filePath, "utf-8");
} catch {
// Fallback to English if Chinese version not found
if (lang) {
try {
const fallback = skill === "monskill"
? join(process.cwd(), "SKILL.md")
: join(process.cwd(), skill, "SKILL.md");
content = readFileSync(fallback, "utf-8");
} catch {
return res.status(404).send("Skill not found");
}
} else {
return res.status(404).send("Skill not found");
}
}
// Fire-and-forget: log the download to Neon
if (process.env.DATABASE_URL) {
const sql = getDb();
const rawIp =
req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
req.headers["x-real-ip"] ||
req.socket?.remoteAddress ||
"unknown";
const ipHash = hashIp(rawIp);
try {
await sql`INSERT INTO skill_downloads (skill_name, ip_hash) VALUES (${skill}, ${ipHash})`;
} catch (e) {
console.error("Failed to log download:", e);
}
}
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Cache-Control", "public, s-maxage=60, stale-while-revalidate=300");
return res.status(200).send(content);
}
import { getDb } from "./_lib/db.js";
export default async function handler(req, res) {
if (!process.env.STATS_SECRET || req.query.key !== process.env.STATS_SECRET) {
return res.status(401).json({ error: "Unauthorized" });
}
if (!process.env.DATABASE_URL) {
return res.status(500).json({ error: "DATABASE_URL not configured" });
}
const sql = getDb();
const [totals, daily, uniqueVisitors] = await Promise.all([
sql`SELECT skill_name, COUNT(*)::int AS downloads
FROM skill_downloads
GROUP BY skill_name
ORDER BY downloads DESC`,
sql`SELECT skill_name, DATE(downloaded_at) AS date, COUNT(*)::int AS downloads
FROM skill_downloads
WHERE downloaded_at > NOW() - INTERVAL '30 days'
GROUP BY skill_name, DATE(downloaded_at)
ORDER BY date DESC, downloads DESC`,
sql`SELECT skill_name, COUNT(DISTINCT ip_hash)::int AS unique_visitors
FROM skill_downloads
GROUP BY skill_name
ORDER BY unique_visitors DESC`,
]);
res.setHeader("Cache-Control", "public, s-maxage=60, stale-while-revalidate=300");
return res.status(200).json({
totals,
unique_visitors: uniqueVisitors,
daily_last_30_days: daily,
});
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Changelog — MONSKILLS</title>
<meta name="description" content="Release notes and changes for monskills.">
<meta property="og:title" content="Changelog — MONSKILLS">
<meta property="og:description" content="Release notes and changes for monskills.">
<meta property="og:url" content="https://skills.devnads.com/changelog">
<meta property="og:type" content="website">
<meta property="og:image" content="https://skills.devnads.com/og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://skills.devnads.com/og.png">
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<link rel="stylesheet" href="shared.css">
<style>
:root {
--bg: #0E091C;
--fg: #c8d0e8;
--dim: #8490b0;
--accent: #8B75FF;
--accent-soft: #c4b5ff;
--border: #2d2245;
--hover: #150e28;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { min-height: 100%; }
body {
background: var(--bg);
color: var(--fg);
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Menlo', 'Consolas', monospace;
font-size: 14px;
line-height: 1.6;
display: flex;
flex-direction: column;
min-height: 100vh;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* Header bar */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1.5rem;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.back-link {
color: var(--dim);
font-size: 13px;
display: inline-flex;
align-items: center;
gap: 0.35rem;
transition: color 0.15s;
}
.back-link:hover { color: var(--fg); text-decoration: none; }
.back-link svg { width: 14px; height: 14px; }
.repo-link {
color: var(--dim);
font-size: 13px;
transition: color 0.15s;
}
.repo-link:hover { color: var(--fg); text-decoration: none; }
/* Page container */
.page {
flex: 1;
width: 100%;
max-width: 820px;
margin: 0 auto;
padding: 4rem 1.5rem 4rem;
}
/* Hero */
.hero {
text-align: center;
margin-bottom: 4rem;
}
.hero-title {
font-size: clamp(64px, 12vw, 140px);
font-weight: 700;
letter-spacing: -0.04em;
line-height: 0.95;
background: linear-gradient(110deg, var(--accent) 40%, var(--accent-soft) 50%, var(--accent) 60%);
background-size: 300% 100%;
background-position: 100% 0;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
text-transform: uppercase;
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Menlo', 'Consolas', monospace;
}
.hero-sub {
color: var(--dim);
font-size: 13px;
margin-top: 1rem;
letter-spacing: 1px;
text-transform: uppercase;
}
/* Timeline */
.timeline {
position: relative;
padding-left: 2.5rem;
--line-x: 1rem; /* shared anchor for both line and dots */
}
.timeline::before {
content: '';
position: absolute;
top: 0.5rem;
bottom: 0.5rem;
left: var(--line-x);
width: 2px;
margin-left: -1px; /* center the 2px line on --line-x */
background: linear-gradient(to bottom, var(--accent) 0%, var(--border) 30%, var(--border) 100%);
}
.entry {
position: relative;
padding-bottom: 3rem;
}
.entry:last-child { padding-bottom: 0; }
.entry::before {
content: '';
position: absolute;
top: 10px;
/* entry sits at padding-left of timeline (2.5rem), so to anchor at --line-x (1rem)
in timeline coords, the dot center sits at -1.5rem in entry coords */
left: calc(var(--line-x) - 2.5rem);
width: 16px;
height: 16px;
margin-left: -8px; /* center the 16px dot on the line */
border-radius: 50%;
background: var(--bg);
border: 2px solid var(--border);
box-sizing: border-box;
transition: border-color 0.2s, box-shadow 0.2s;
}
.entry.latest::before {
border-color: var(--accent);
box-shadow: 0 0 0 4px rgba(139, 117, 255, 0.15);
}
.entry-head {
display: flex;
align-items: baseline;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 0.75rem;
}
.entry-version {
font-size: 20px;
font-weight: 600;
color: var(--accent);
letter-spacing: -0.01em;
}
.entry-version a { color: inherit; }
.entry-version a:hover { text-decoration: none; opacity: 0.85; }
.entry-date {
color: var(--dim);
font-size: 12px;
letter-spacing: 0.5px;
}
.entry-badge {
display: inline-block;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
padding: 0.15rem 0.5rem;
border-radius: 3px;
background: rgba(139, 117, 255, 0.12);
color: var(--accent);
border: 1px solid rgba(139, 117, 255, 0.3);
}
.entry-badge.pre {
background: rgba(212, 96, 95, 0.12);
color: #d4605f;
border-color: rgba(212, 96, 95, 0.35);
}
.entry-body {
border: 1px solid var(--border);
border-radius: 6px;
padding: 1.25rem 1.5rem;
background: rgba(21, 14, 40, 0.4);
transition: border-color 0.2s;
}
.entry.latest .entry-body { border-color: rgba(139, 117, 255, 0.35); }
.entry-body h1, .entry-body h2, .entry-body h3 {
color: var(--accent);
margin: 1.1em 0 0.5em;
font-weight: 600;
}
.entry-body h1 { font-size: 1.2em; }
.entry-body h2 { font-size: 1.08em; }
.entry-body h3 { font-size: 1em; }
.entry-body > *:first-child { margin-top: 0; }
.entry-body > *:last-child { margin-bottom: 0; }
.entry-body p { margin: 0.65em 0; }
.entry-body ul, .entry-body ol { margin: 0.65em 0; padding-left: 1.5em; }
.entry-body li { margin: 0.3em 0; }
.entry-body code {
background: var(--hover);
color: var(--accent);
padding: 0.1em 0.4em;
border-radius: 3px;
font-size: 0.92em;
}
.entry-body pre {
background: var(--hover);
border: 1px solid var(--border);
border-radius: 3px;
padding: 0.75rem 1rem;
overflow-x: auto;
margin: 0.75em 0;
}
.entry-body pre code { background: none; padding: 0; color: var(--fg); }
.entry-body strong { color: var(--fg); font-weight: 600; }
.entry-body a { color: var(--accent); }
.entry-body blockquote {
border-left: 3px solid var(--accent);
margin: 0.75em 0;
padding-left: 1em;
color: var(--dim);
}
.empty-body {
color: var(--dim);
font-style: italic;
font-size: 13px;
}
.state {
text-align: center;
padding: 4rem 1rem;
color: var(--dim);
}
.state.error { color: #d4605f; }
.state a { color: var(--accent); }
.footer {
text-align: center;
color: var(--dim);
font-size: 11px;
padding: 1.25rem 1.5rem;
border-top: 1px solid var(--border);
}
/* Mobile */
@media (max-width: 600px) {
.page { padding: 2.5rem 1rem 2.5rem; }
.hero { margin-bottom: 2.5rem; }
.timeline { padding-left: 1.75rem; --line-x: 0.625rem; }
.entry::before { width: 12px; height: 12px; margin-left: -6px; top: 8px; }
.entry-body { padding: 1rem 1.1rem; }
}
</style>
</head>
<body>
<header class="header">
<a href="/" class="back-link">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
Back
</a>
<a class="repo-link" href="https://github.com/therealharpaljadeja/monskills/releases" target="_blank" rel="noopener">Releases on GitHub ↗</a>
</header>
<main class="page">
<section class="hero">
<h1 class="hero-title" id="hero-title">Changelog</h1>
<div class="hero-sub">What shipped, when</div>
</section>
<section id="timeline" class="timeline" aria-live="polite">
<div class="state" id="loading">Loading releases…</div>
</section>
</main>
<footer class="footer">
MIT License · <a href="/" >skills.devnads.com</a>
</footer>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.7/dist/gsap.min.js"></script>
<script>
const REPO = 'therealharpaljadeja/monskills';
const API = 'https://api.github.com/repos/' + REPO + '/releases?per_page=50';
const timelineEl = document.getElementById('timeline');
function escHtml(s) {
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
function renderMarkdown(md) {
if (!md || !md.trim()) return '';
const esc = escHtml;
const inline = s => esc(s)
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/(^|[\s(])\*(?!\s)([^*\n]+?)\*(?=[\s).,!?:;]|$)/g, '$1<em>$2</em>')
.replace(/`([^`\n]+)`/g, '<code>$1</code>')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
const safeUrl = /^https?:\/\//.test(url) || url.startsWith('/') || url.startsWith('#') ? url : '#';
return '<a href="' + safeUrl + '" target="_blank" rel="noopener">' + text + '</a>';
});
// Normalize: convert CRLF, then strip trailing whitespace from each line.
// GitHub release bodies often have huge trailing-whitespace runs that don't
// affect rendering but make the source noisy. Leading whitespace is preserved
// only inside fenced code blocks.
let raw = md.replace(/\r\n?/g, '\n');
const lines = raw.split('\n').map(l => l.replace(/\s+$/, ''));
let out = '';
let i = 0;
while (i < lines.length) {
const line = lines[i];
if (/^\s*```(\w*)\s*$/.test(line)) {
let block = '';
for (i++; i < lines.length && !/^\s*```\s*$/.test(lines[i]); i++) block += esc(lines[i]) + '\n';
i++;
out += '<pre><code>' + block + '</code></pre>';
continue;
}
if (/^\s*(#{1,3})\s+(.+?)\s*#*$/.test(line)) {
const l = RegExp.$1.length;
out += '<h' + l + '>' + inline(RegExp.$2) + '</h' + l + '>';
i++;
continue;
}
if (/^\s*>\s?(.*)$/.test(line)) {
let block = inline(RegExp.$1);
for (i++; i < lines.length && /^\s*>\s?(.*)$/.test(lines[i]); i++) block += '\n' + inline(RegExp.$1);
out += '<blockquote>' + block.replace(/\n/g, '<br>') + '</blockquote>';
continue;
}
if (/^\s*[-*]\s+(.+)$/.test(line)) {
out += '<ul>';
while (i < lines.length && /^\s*[-*]\s+(.+)$/.test(lines[i])) {
out += '<li>' + inline(RegExp.$1) + '</li>';
i++;
}
out += '</ul>';
continue;
}
if (/^\s*\d+\.\s+(.+)$/.test(line)) {
out += '<ol>';
while (i < lines.length && /^\s*\d+\.\s+(.+)$/.test(lines[i])) {
out += '<li>' + inline(RegExp.$1) + '</li>';
i++;
}
out += '</ol>';
continue;
}
if (line.trim() === '') { i++; continue; }
// Coalesce consecutive non-empty lines into one paragraph (join with space).
let para = line.trim();
for (i++; i < lines.length && lines[i].trim() !== '' && !/^\s*(#{1,3}\s|>|[-*]\s|\d+\.\s|```)/.test(lines[i]); i++) {
para += ' ' + lines[i].trim();
}
out += '<p>' + inline(para) + '</p>';
}
return out;
}
function formatDate(iso) {
if (!iso) return '';
const d = new Date(iso);
if (isNaN(d.getTime())) return '';
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
}
function renderReleases(releases) {
if (!releases.length) {
timelineEl.innerHTML = '<div class="state">No releases published yet.</div>';
return;
}
const items = releases.map((r, idx) => {
const title = r.name && r.name.trim() ? r.name : r.tag_name;
const date = formatDate(r.published_at || r.created_at);
const body = renderMarkdown(r.body || '') || '<p class="empty-body">No release notes.</p>';
const badge = r.prerelease
? '<span class="entry-badge pre">Pre-release</span>'
: (idx === 0 ? '<span class="entry-badge">Latest</span>' : '');
return (
'<article class="entry' + (idx === 0 ? ' latest' : '') + '">' +
'<div class="entry-head">' +
'<h2 class="entry-version"><a href="' + escHtml(r.html_url) + '" target="_blank" rel="noopener">' + escHtml(title) + '</a></h2>' +
(date ? '<span class="entry-date">' + escHtml(date) + '</span>' : '') +
badge +
'</div>' +
'<div class="entry-body">' + body + '</div>' +
'</article>'
);
}).join('');
timelineEl.innerHTML = items;
if (window.gsap) {
gsap.from('.entry', {
opacity: 0,
y: 16,
duration: 0.45,
ease: 'power2.out',
stagger: 0.07
});
}
}
function renderError(message) {
timelineEl.innerHTML =
'<div class="state error">' +
'Could not load releases: ' + escHtml(message) + '<br>' +
'<a href="https://github.com/' + REPO + '/releases" target="_blank" rel="noopener">View on GitHub ↗</a>' +
'</div>';
}
fetch(API, { headers: { 'Accept': 'application/vnd.github+json' } })
.then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(data => {
const releases = (data || []).filter(r => !r.draft);
releases.sort((a, b) => new Date(b.published_at || b.created_at) - new Date(a.published_at || a.created_at));
renderReleases(releases);
})
.catch(err => renderError(err.message || 'Unknown error'));
// Looping shine on the hero title
if (window.gsap) {
gsap.fromTo('#hero-title',
{ backgroundPosition: '100% 0' },
{ backgroundPosition: '0% 0', duration: 2.5, ease: 'power1.inOut', repeat: -1, repeatDelay: 3 }
);
}
</script>
</body>
</html>
SKILL.md
Submit anonymous feedback about monskills using the feedback skill.
User-provided message (may be empty): "$ARGUMENTS"
Steps:
1. If the user's message is empty, ask them one short question: what is wrong with monskills right now? Wait for their reply before continuing. 2. Invoke the feedback skill via the Skill tool and follow its privacy rules strictly. Scrub the payload of any keys, addresses the user owns, hostnames, or file paths outside the project. 3. Decide the category from the user's message:
- skill content is wrong →
incorrect-info - skill is missing something →
suggestion - agent kept failing →
error-looporstuck - general complaint about monskills →
user-complaint - anything else →
other
4. Pick severity: high if the user is blocked, medium if there is a workaround, low for nits. 5. If the feedback is about a specific skill (wallet, scaffold, addresses, concepts, gas, wallet-integration, tooling-and-infra, why-monad, feedback), include it as skill. Otherwise omit. 6. POST to https://skills.devnads.com/api/feedback with Content-Type: application/json and a body containing at minimum source: "user", message, and whichever optional fields you determined. Use curl -sS -X POST. 7. Report the returned id back to the user in one line, e.g. Filed anonymous feedback #482. If the response is not ok, show the error verbatim and stop — do not retry more than once.
Do not invent facts that the user did not say. Do not include this command's own argument value verbatim if it contains anything that looks like a secret ask the user to rephrase instead.
Asynchronous Execution
Monad decouples consensus from execution. Nodes agree on transaction ordering without executing transactions first. Execution runs in parallel with consensus rather than blocking it.
What this means for developers:
- Consensus operates with a 3-block delayed state view (
D=3). The state root included in a block is from 3 blocks prior. - Newly funded accounts cannot send transactions until their funding transfer is `D` blocks old (~1.2 seconds after the transaction is included). This is because consensus validates gas budgets against the delayed state, and the funding won't be visible yet.
- Workaround: Use a smart contract to atomically combine funding and spending in a single transaction, bypassing the delay.
- Despite the lag, the true state is deterministic as soon as ordering is determined — execution simply catches up.
eth_callandeth_estimateGassimulate against speculatively executed state, so they return accurate results even though execution technically lags consensus.
Reference: https://docs.monad.xyz/monad-arch/consensus/asynchronous-execution
异步执行
Monad 将共识与执行解耦。节点在不先执行交易的情况下就交易排序达成一致。执行与共识并行运行,而不是阻塞共识。
这对开发者意味着什么:
- 共识使用延迟 3 个区块的状态视图(
D=3)。区块中包含的状态根来自 3 个区块之前。 - 新充值账户在其资金转入交易经过 `D` 个区块后才能发送交易(交易被包含后约 1.2 秒)。这是因为共识根据延迟状态验证 Gas 预算,而资金在此之前尚不可见。
- 解决方案: 使用智能合约在单笔交易中原子性地组合充值和消费操作,从而绕过延迟。
- 尽管存在延迟,一旦排序确定,真实状态就是确定性的 — 执行只是在追赶进度。
eth_call和eth_estimateGas基于推测性执行的状态进行模拟,因此即使执行在技术上滞后于共识,它们仍返回准确的结果。
参考:https://docs.monad.xyz/monad-arch/consensus/asynchronous-execution
Block States
Monad blocks progress through four states. Each maps to a familiar Ethereum JSON-RPC tag:
| State | Description | JSON-RPC Tag |
|---|---|---|
| Proposed | Leader proposed the block, no votes yet. Speculatively executed. | "latest" |
| Voted | Supermajority of validators voted affirmatively (Quorum Certificate). | "safe" |
| Finalized | QC-squared exists — irreversible without a hard fork. | "finalized" |
| Verified | Delayed merkle root finalized — execution outputs agreed upon by supermajority. | — |
What this means for developers:
- Use
"latest"for fast reads (proposed state, speculative). - Use
"safe"for data that has validator backing but could theoretically revert. - Use
"finalized"for irreversible actions (e.g. confirming a withdrawal). - Proposed blocks undergo speculative execution. In rare cases, apps consuming real-time data may see data from blocks that don't become canonical.
- With 400ms block time and 800ms finality, the progression through these states is very fast.
Reference: https://docs.monad.xyz/monad-arch/consensus/block-states
区块状态
Monad 区块经历四个状态。每个状态映射到一个常见的以太坊 JSON-RPC 标签:
| 状态 | 描述 | JSON-RPC 标签 |
|---|---|---|
| Proposed | 领导者提议了区块,尚无投票。已推测性执行。 | "latest" |
| Voted | 超过三分之二的验证者投了赞成票(法定人数证书)。 | "safe" |
| Finalized | 存在 QC-squared — 不经硬分叉则不可逆。 | "finalized" |
| Verified | 延迟的 Merkle 根已最终确认 — 执行输出由超多数验证者达成一致。 | — |
这对开发者意味着什么:
- 使用
"latest"进行快速读取(提议状态,推测性的)。 - 使用
"safe"获取有验证者支持但理论上仍可能回退的数据。 - 使用
"finalized"进行不可逆操作(例如确认提款)。 - 提议区块会进行推测性执行。在极少数情况下,消费实时数据的应用可能会看到来自未成为规范链的区块的数据。
- 由于 400 毫秒的出块时间和 800 毫秒的最终确认时间,这些状态之间的转换非常快。
参考:https://docs.monad.xyz/monad-arch/consensus/block-states
EIP-7702 (Delegated EOAs)
EIP-7702 lets EOAs delegate code execution to a smart contract, gaining smart wallet capabilities (multisig, social recovery, session keys, gas sponsorship) without migrating to a new account.
How it works:
1. EOA signs an authorization message pointing to a contract address. 2. A type 0x04 transaction submits this authorization (can be sent by the EOA or a third party / gas sponsor). 3. The EOA becomes "delegated" and behaves as if it has the delegated contract's code.
Monad-specific behaviors:
- Balance floor: Delegated EOAs cannot have transactions reduce their balance below 10 MON. If the balance is already below 10 MON but unchanged or increased by the transaction, it succeeds.
- No CREATE/CREATE2: Contract code executing within a delegated EOA's context cannot use
CREATEorCREATE2opcodes (the call frame reverts). This prevents nonce manipulation. Standard contract-creation transactions from the EOA are still allowed. - Delegations persist indefinitely until explicitly cleared (send a
0x04transaction with0x0000...0000as the delegate address).
viem example:
import { createWalletClient, http } from 'viem'
import { monadTestnet } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
const account = privateKeyToAccount('0x...')
const walletClient = createWalletClient({
account,
chain: monadTestnet,
transport: http(),
})
const authorization = await walletClient.signAuthorization({
account,
contractAddress: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2',
})
const hash = await walletClient.sendTransaction({
authorizationList: [authorization],
data: '0xdeadbeef',
to: walletClient.account.address,
})Reference: https://docs.monad.xyz/developer-essentials/eip-7702
EIP-7702(委托 EOA)
EIP-7702 允许 EOA 将代码执行委托给智能合约,从而获得智能钱包功能(多签、社交恢复、会话密钥、Gas 赞助),而无需迁移到新账户。
工作原理:
1. EOA 签署一条指向合约地址的授权消息。 2. 通过类型 0x04 的交易提交此授权(可由 EOA 自身或第三方/Gas 赞助者发送)。 3. EOA 变为"已委托"状态,表现得如同拥有被委托合约的代码。
Monad 特定行为:
- 余额下限: 委托 EOA 的交易不能将其余额降低到 10 MON 以下。如果余额已低于 10 MON,但交易未减少或增加了余额,则交易成功。
- 禁止 CREATE/CREATE2: 在委托 EOA 上下文中执行的合约代码不能使用
CREATE或CREATE2操作码(调用帧将回退)。这是为了防止 nonce 被操纵。EOA 发起的标准合约创建交易仍然允许。 - 委托将无限期持续,直到被明确清除(发送一笔
0x04类型交易,将委托地址设为0x0000...0000)。
viem 示例:
import { createWalletClient, http } from 'viem'
import { monadTestnet } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
const account = privateKeyToAccount('0x...')
const walletClient = createWalletClient({
account,
chain: monadTestnet,
transport: http(),
})
const authorization = await walletClient.signAuthorization({
account,
contractAddress: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2',
})
const hash = await walletClient.sendTransaction({
authorizationList: [authorization],
data: '0xdeadbeef',
to: walletClient.account.address,
})参考:https://docs.monad.xyz/developer-essentials/eip-7702
Execution Events and Consensus Events
Monad emits two categories of events through its real-time data feeds:
Consensus events announce block state transitions:
BLOCK_START— Block execution begins (Proposed state). Includes block tag, round, epoch.BLOCK_QC— Block received a quorum certificate (Voted state).BLOCK_FINALIZED— Block is finalized (irreversible).BLOCK_VERIFIED— State root verified by supermajority.
Execution events are EVM trace data (logs, state changes) emitted during block processing. They are speculative — the block they belong to might not become canonical.
What this means for developers:
- If consuming real-time data, always track consensus events to know which block state you're seeing.
- Data from
BLOCK_STARTis speculative. Only treat data as final afterBLOCK_FINALIZED. - Block identification requires both a consensus ID and proposed block number (communicated via "block tags").
Reference: https://docs.monad.xyz/execution-events/consensus-events
执行事件和共识事件
Monad 通过其实时数据流发出两类事件:
共识事件通告区块状态转换:
BLOCK_START— 区块开始执行(Proposed 状态)。包含区块标签、轮次、纪元。BLOCK_QC— 区块收到法定人数证书(Voted 状态)。BLOCK_FINALIZED— 区块已最终确认(不可逆)。BLOCK_VERIFIED— 状态根已由超多数验证者验证。
执行事件是区块处理过程中产生的 EVM 追踪数据(日志、状态变更)。它们是推测性的 — 其所属的区块可能不会成为规范链的一部分。
这对开发者意味着什么:
- 如果消费实时数据,务必追踪共识事件以了解当前看到的是哪个区块状态。
- 来自
BLOCK_START的数据是推测性的。只有在BLOCK_FINALIZED之后才应将数据视为最终确认。 - 区块识别需要同时使用共识 ID 和提议区块号(通过"区块标签"传达)。
参考:https://docs.monad.xyz/execution-events/consensus-events
Parallel Execution
Monad executes transactions in parallel using optimistic concurrency control, but the final result is identical to sequential Ethereum execution. Transaction ordering within a block is preserved.
What this means for developers:
- No code changes required. Existing Solidity contracts work as-is. The execution semantics are identical to Ethereum.
- Monad starts executing transactions optimistically before predecessors complete. It tracks reads and compares them against prior writes. If a conflict is detected, the transaction is re-executed with correct state.
- Expensive computations like signature recovery and state lookups are cached, so re-execution overhead is minimal.
- Contracts that touch frequently-updated storage slots (e.g. a single global counter) may cause more re-executions, but this is handled transparently — it only affects node performance, not correctness.
Reference: https://docs.monad.xyz/monad-arch/execution/parallel-execution
并行执行
Monad 使用乐观并发控制并行执行交易,但最终结果与以太坊顺序执行完全一致。区块内的交易排序保持不变。
这对开发者意味着什么:
- 无需修改代码。 现有的 Solidity 合约可以直接使用。执行语义与以太坊完全相同。
- Monad 在前序交易完成之前就开始乐观执行交易。它追踪读取操作并与先前的写入操作进行比较。如果检测到冲突,交易将使用正确的状态重新执行。
- 昂贵的计算(如签名恢复和状态查找)会被缓存,因此重新执行的开销很小。
- 频繁更新存储槽的合约(例如单一的全局计数器)可能会导致更多的重新执行,但这是透明处理的 — 它只影响节点性能,不影响正确性。
参考:https://docs.monad.xyz/monad-arch/execution/parallel-execution
Real-Time Data Sources
Monad's high throughput (~10,000 tps) makes traditional JSON-RPC polling impractical. Three real-time data sources are available:
1. Geth-Compatible WebSocket Events
- Standard
eth_subscribewithnewHeadsandlogs. - Data publishes at Proposed state.
- Available via third-party RPC providers (Alchemy, QuickNode, etc.).
- Best for: apps migrating from Ethereum with minimal changes.
2. Monad Extended WebSocket Events
monadNewHeadsandmonadLogssubscriptions.- Data publishes at Proposed state (earlier than standard).
- Includes consensus progression tracking.
- Best for: apps that need the lowest latency and can handle speculative data.
3. Execution Events SDK (C/C++/Rust)
- Transaction-level granularity with logs, call frames, and state reads/writes.
- Fastest option — powers the other two sources internally.
- Requires running a custom program on the same host as a Monad node.
- Best for: indexers, analytics, MEV, and high-performance infrastructure.
Which to choose:
- Most app developers should use Source 1 (Geth-compatible) via their RPC provider.
- Use Source 2 if you need earlier data and understand speculative execution.
- Use Source 3 only if you run your own node and need maximum performance.
Reference: https://docs.monad.xyz/monad-arch/realtime-data/data-sources
实时数据源
Monad 的高吞吐量(约 10,000 tps)使传统的 JSON-RPC 轮询变得不切实际。有三种实时数据源可用:
1. Geth 兼容 WebSocket 事件
- 标准的
eth_subscribe,支持newHeads和logs。 - 数据在 Proposed 状态时发布。
- 通过第三方 RPC 提供商(Alchemy、QuickNode 等)提供。
- 最适合:从以太坊迁移且需要最小改动的应用。
2. Monad 扩展 WebSocket 事件
monadNewHeads和monadLogs订阅。- 数据在 Proposed 状态时发布(比标准更早)。
- 包含共识进展追踪。
- 最适合:需要最低延迟且能处理推测性数据的应用。
3. 执行事件 SDK(C/C++/Rust)
- 交易级别的粒度,包含日志、调用帧和状态读写。
- 最快的选项 — 在内部驱动其他两种来源。
- 需要在与 Monad 节点相同的主机上运行自定义程序。
- 最适合:索引器、分析、MEV 和高性能基础设施。
如何选择:
- 大多数应用开发者应通过 RPC 提供商使用来源 1(Geth 兼容)。
- 如果需要更早的数据且了解推测性执行,使用来源 2。
- 仅当运行自己的节点且需要最大性能时,使用来源 3。
参考:https://docs.monad.xyz/monad-arch/realtime-data/data-sources
Reserve Balance
Reserve balance is a safety mechanism that prevents transactions from failing due to insufficient gas in the asynchronous execution model. It sets a 10 MON floor per EOA.
What this means for developers:
- Most apps won't be affected. This only matters for accounts with low MON balances.
- Transactions revert if an account's ending balance (before gas refunds) drops below
min(starting_balance, 10 MON). - Low-balance accounts (below 10 MON) can only send one transaction every 3 blocks (~1.2 seconds).
- An emptying transaction exception exists: undelegated accounts that sent no other transactions in the past 3 blocks can spend below the reserve. This allows users to fully withdraw their balance.
- EIP-7702 delegated accounts cannot use the emptying exception — they are always subject to the 10 MON floor when their balance decreases.
- For transaction senders, consensus enforces a cumulative gas budget across all inflight transactions (past 3 blocks):
min(10 MON, lagged_state_balance).
Reference: https://docs.monad.xyz/developer-essentials/reserve-balance
储备余额
储备余额是一种安全机制,用于防止交易在异步执行模型中因 Gas 不足而失败。它为每个 EOA 设置了 10 MON 的余额下限。
这对开发者意味着什么:
- 大多数应用不会受到影响。 这仅对 MON 余额较低的账户有影响。
- 如果账户的最终余额(Gas 退款之前)低于
min(起始余额, 10 MON),交易将回退。 - 低余额账户(低于 10 MON)每 3 个区块(约 1.2 秒)只能发送一笔交易。
- 存在清空交易例外:未委托的账户如果在过去 3 个区块内没有发送其他交易,可以消费低于储备余额的金额。这允许用户完全提取其余额。
- EIP-7702 委托账户不能使用清空例外 — 当其余额减少时,始终受 10 MON 下限约束。
- 对于交易发送者,共识在所有进行中的交易(过去 3 个区块)上强制执行累计 Gas 预算:
min(10 MON, 延迟状态余额)。
参考:https://docs.monad.xyz/developer-essentials/reserve-balance
Monad 与以太坊兼容,但其架构引入了开发者必须了解的行为差异。请仅获取与当前任务相关的参考资料。
按任务获取
| 我遇到的问题... | 获取 |
|---|---|
| 新充值账户无法发送交易,资金到账延迟 | async-execution.zh.md |
| 现有 Solidity 合约是否需要为 Monad 做修改 | parallel-execution.zh.md |
选择 latest、safe、finalized 区块标签 | block-states.zh.md |
| 交易因余额不足回退、10 MON 下限、清空交易 | reserve-balance.zh.md |
| 智能钱包委托、EIP-7702、会话密钥、Gas 赞助 | eip-7702.zh.md |
| 订阅事件、WebSocket 数据流、高吞吐量数据接入 | realtime-data.zh.md |
| 区块生命周期事件、推测性数据、BLOCK_START/QC/FINALIZED | execution-events.zh.md |
概要
- 异步执行: 共识与执行解耦。状态视图有 3 个区块的延迟。新充值账户需等待约 1.2 秒才能发送交易。
- 并行执行: 乐观并发控制 — 结果与以太坊完全一致。无需修改合约。
- 区块状态: Proposed → Voted → Finalized → Verified。分别映射到
latest/safe/finalized。 - 储备余额: 每个 EOA 有 10 MON 的余额下限。低余额账户每约 1.2 秒限发送 1 笔交易。
- EIP-7702: EOA 可委托给合约以获得智能钱包功能。10 MON 下限适用。在委托上下文中不可使用 CREATE/CREATE2。
- 实时数据: 3 种来源 — Geth 兼容 WS、Monad 扩展 WS、执行事件 SDK。大多数应用使用来源 1。
- 执行事件: 共识事件追踪区块状态转换。执行事件是推测性的 EVM 追踪数据。
ADR-001: Static Markdown Skill Distribution
Status
Accepted
Context
MONSKILLS distributes domain-specific knowledge (Monad development patterns, contract addresses, deployment guides) to AI agents as Claude Code skills.
The Claude Code skills specification requires skills to be markdown files (SKILL.md) served over HTTP. This is not an architectural choice — it is a requirement.
Decision
Use static markdown files (SKILL.md) served over HTTP with CORS headers, following the Claude Code skills specification.
Consequences
Positive
- Spec-compliant — Works out of the box with Claude Code, Cursor, Codex, Copilot, and any agent that supports the skills spec.
- Zero dependencies for consumers — Any HTTP client (curl, fetch, agent) can read a skill. No SDK, no auth, no package install.
- Version controlled — Skills are plain files in git. Changes are reviewed via PRs with full diff history.
- Cacheable — Static files work naturally with CDN caching.
- Simple hosting — Any static host (Vercel, Netlify, GitHub Pages) works.
Negative
- No structured metadata API — Consumers can't query "which skills exist?" programmatically without fetching the index.
- No partial fetching — Agents must fetch entire skill files, even if they only need a section.
- Manual updates — Adding a skill requires a code change and deploy, not a CMS update.
Neutral
- Markdown rendering for the landing page modal is done client-side with a minimal custom parser (no external dependency).
ADR-002: Anonymous IP Tracking with Daily Hash Rotation
Status
Accepted
Context
The maintainer needs to understand which skills are most downloaded and how many unique users access them. This requires some form of visitor deduplication.
Options considered:
1. Raw IP storage — Store full IP addresses in the database. 2. Hashed IP with static salt — SHA-256 hash the IP with a fixed salt. 3. Hashed IP with daily rotating salt — SHA-256 hash the IP with the current date as salt. 4. No deduplication — Count raw hits without any visitor identification. 5. Cookie-based tracking — Set a unique cookie per visitor.
Decision
Use SHA-256 hashed IPs with a daily rotating salt: SHA-256(ip + "YYYY-MM-DD").
Consequences
Positive
- Privacy preserving — Raw IPs never reach the database. The hash is irreversible.
- Daily deduplication — Same IP on the same day produces the same hash, enabling unique visitor counts per day.
- Cross-day unlinkability — Different days produce different hashes, so visitors cannot be tracked across days.
- No cookies — Works for programmatic clients (curl, agents) that don't support cookies.
- No user consent required — No PII is stored, no cookies are set.
Negative
- No cross-day unique counts — Cannot answer "how many unique users in the last 30 days" because hashes change daily. Only "unique visitors per day" is possible.
- Shared IP collisions — Users behind the same NAT/VPN will be counted as one unique visitor per day. Acceptable for this use case.
- Salt is predictable — The date is public knowledge, so an attacker with access to the database could theoretically brute-force common IPs against known dates. Mitigated by the fact that this data has low sensitivity (it only reveals "someone downloaded a public skill").
Neutral
- User-agent strings are not stored, further reducing any fingerprinting surface.
- The footer on the landing page discloses that anonymous tracking is in place.
ADR-003: Vercel Routes for Download Tracking
Status
Accepted
Context
Skills are markdown files on disk. To track downloads, requests must pass through a serverless function before the content is returned. Vercel offers several routing mechanisms:
Options considered:
1. `rewrites` in vercel.json — Maps URL patterns to destinations. Evaluated after static file matching. 2. `routes` in vercel.json — Legacy routing config. Evaluated before static file matching. 3. Edge Middleware — Intercepts requests at the edge. Available for all frameworks but adds complexity. 4. Client-side tracking — Add a fetch call in the landing page JavaScript. Only tracks browser visits, not programmatic downloads.
Decision
Use routes in vercel.json to intercept skill URLs and route them through /api/skill.js before Vercel checks for static files.
Consequences
Positive
- Captures all downloads — Browser visits, curl, agent fetches, and direct SKILL.md URLs all pass through the tracking function.
- Transparent — The URL doesn't change for the consumer.
/scaffoldstill returns markdown. - Simple — No middleware runtime, no edge function config. Just a routing rule and a serverless function.
Negative
- Added latency — Every skill download now goes through a serverless function instead of being served directly from the CDN. Mitigated by
Cache-Control: s-maxage=60, stale-while-revalidate=300. - Cold starts — First request after idle may have ~200-500ms cold start for the serverless function.
- `routes` is legacy config — Vercel recommends
rewrites/redirectsfor new projects. However,rewritesevaluate after static files, which defeats the purpose. Theroutesconfig is stable and still supported. - Skill allowlist maintenance — New skills must be added to both the
routesregex invercel.jsonand theVALID_SKILLSarray inapi/skill.js.
Neutral
- The
includeFilesfunction config ensures SKILL.md files are bundled with the serverless function forreadFileSyncaccess. - The function reads files synchronously, which is acceptable for small markdown files in a serverless context.
openapi: 3.0.3
info:
title: MONSKILLS API
description: API for serving Monad development skills and viewing download analytics.
version: 1.0.0
contact:
name: Harpalsinh Jadeja
url: https://github.com/therealharpaljadeja/monskills
servers:
- url: https://skills.devnads.com
description: Production
paths:
/api/skill:
get:
summary: Get a skill's markdown content
description: |
Returns the SKILL.md file for the requested skill.
Also logs an anonymous download event (hashed IP + skill name) to the database.
parameters:
- name: name
in: query
required: true
description: The skill identifier
schema:
type: string
enum:
- monskill
- scaffold
- why-monad
- addresses
- wallet
- wallet-integration
responses:
"200":
description: Skill markdown content
headers:
Content-Type:
schema:
type: string
example: text/markdown; charset=utf-8
Access-Control-Allow-Origin:
schema:
type: string
example: "*"
Cache-Control:
schema:
type: string
example: public, s-maxage=60, stale-while-revalidate=300
content:
text/markdown:
schema:
type: string
example: |
---
name: scaffold
description: End-to-end guide from idea to production.
---
# Scaffold
...
"404":
description: Skill not found
content:
text/plain:
schema:
type: string
example: Skill not found
/api/stats:
get:
summary: Get download analytics
description: |
Returns aggregated download statistics per skill.
Protected by a secret key passed as a query parameter.
parameters:
- name: key
in: query
required: true
description: The stats secret key (must match STATS_SECRET env var)
schema:
type: string
responses:
"200":
description: Analytics data
content:
application/json:
schema:
type: object
properties:
totals:
type: array
items:
type: object
properties:
skill_name:
type: string
example: scaffold
downloads:
type: integer
example: 142
unique_visitors:
type: array
items:
type: object
properties:
skill_name:
type: string
example: scaffold
unique_visitors:
type: integer
example: 98
daily_last_30_days:
type: array
items:
type: object
properties:
skill_name:
type: string
example: scaffold
date:
type: string
format: date
example: "2026-03-18"
downloads:
type: integer
example: 12
"401":
description: Unauthorized — missing or incorrect key
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: Unauthorized
"500":
description: Server error — DATABASE_URL not configured
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: DATABASE_URL not configured
/{skill}:
get:
summary: Skill shorthand URL
description: |
Convenience route that rewrites to /api/skill?name={skill}.
Example: GET /scaffold → served by /api/skill?name=scaffold
parameters:
- name: skill
in: path
required: true
schema:
type: string
enum:
- scaffold
- why-monad
- addresses
- wallet
- wallet-integration
responses:
"200":
description: Skill markdown content (same as /api/skill)
content:
text/markdown:
schema:
type: string
/{skill}/SKILL.md:
get:
summary: Skill direct file URL
description: |
Direct path to skill file, also routed through tracking.
Example: GET /scaffold/SKILL.md → served by /api/skill?name=scaffold
parameters:
- name: skill
in: path
required: true
schema:
type: string
enum:
- scaffold
- why-monad
- addresses
- wallet
- wallet-integration
responses:
"200":
description: Skill markdown content (same as /api/skill)
content:
text/markdown:
schema:
type: string
/SKILL.md:
get:
summary: Root skill index
description: |
Returns the master SKILL.md index file.
Tracked as skill name "monskill".
responses:
"200":
description: Master skill index markdown
content:
text/markdown:
schema:
type: string
{
"type": "excalidraw",
"version": 2,
"source": "monskills",
"elements": [
{
"id": "title",
"type": "text",
"x": 300,
"y": 20,
"width": 400,
"height": 40,
"text": "MONSKILLS — System Context (C4 Level 1)",
"fontSize": 24,
"fontFamily": 1,
"textAlign": "center",
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 1,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false
},
{
"id": "agent-box",
"type": "rectangle",
"x": 50,
"y": 100,
"width": 180,
"height": 100,
"strokeColor": "#1971c2",
"backgroundColor": "#d0ebff",
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 2,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false,
"roundness": { "type": 3 }
},
{
"id": "agent-label",
"type": "text",
"x": 80,
"y": 120,
"width": 120,
"height": 60,
"text": "AI Agent\n(Claude, Cursor,\nCodex)",
"fontSize": 14,
"fontFamily": 1,
"textAlign": "center",
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 3,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false
},
{
"id": "developer-box",
"type": "rectangle",
"x": 50,
"y": 260,
"width": 180,
"height": 100,
"strokeColor": "#1971c2",
"backgroundColor": "#d0ebff",
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 4,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false,
"roundness": { "type": 3 }
},
{
"id": "developer-label",
"type": "text",
"x": 85,
"y": 285,
"width": 110,
"height": 50,
"text": "Developer\n(Browser)",
"fontSize": 14,
"fontFamily": 1,
"textAlign": "center",
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 5,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false
},
{
"id": "maintainer-box",
"type": "rectangle",
"x": 50,
"y": 420,
"width": 180,
"height": 100,
"strokeColor": "#1971c2",
"backgroundColor": "#d0ebff",
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 6,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false,
"roundness": { "type": 3 }
},
{
"id": "maintainer-label",
"type": "text",
"x": 100,
"y": 450,
"width": 80,
"height": 40,
"text": "Maintainer",
"fontSize": 14,
"fontFamily": 1,
"textAlign": "center",
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 7,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false
},
{
"id": "monskills-box",
"type": "rectangle",
"x": 400,
"y": 150,
"width": 250,
"height": 160,
"strokeColor": "#2f9e44",
"backgroundColor": "#d8f5a2",
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 8,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false,
"roundness": { "type": 3 }
},
{
"id": "monskills-label",
"type": "text",
"x": 440,
"y": 170,
"width": 170,
"height": 120,
"text": "MONSKILLS\n(Vercel)\n\nStatic landing page\nServerless functions\nSkill markdown files",
"fontSize": 14,
"fontFamily": 1,
"textAlign": "center",
"strokeColor": "#2f9e44",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 9,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false
},
{
"id": "neon-box",
"type": "rectangle",
"x": 750,
"y": 200,
"width": 200,
"height": 100,
"strokeColor": "#e8590c",
"backgroundColor": "#fff4e6",
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 10,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false,
"roundness": { "type": 3 }
},
{
"id": "neon-label",
"type": "text",
"x": 785,
"y": 225,
"width": 130,
"height": 50,
"text": "Neon PostgreSQL\n(Serverless DB)",
"fontSize": 14,
"fontFamily": 1,
"textAlign": "center",
"strokeColor": "#e8590c",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 11,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false
},
{
"id": "arrow-agent-monskills",
"type": "arrow",
"x": 230,
"y": 150,
"width": 170,
"height": 50,
"points": [[0, 0], [170, 50]],
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 12,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false,
"startArrowhead": null,
"endArrowhead": "arrow"
},
{
"id": "arrow-agent-label",
"type": "text",
"x": 260,
"y": 140,
"width": 130,
"height": 30,
"text": "GET /scaffold\n→ text/markdown",
"fontSize": 11,
"fontFamily": 1,
"textAlign": "center",
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 13,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false
},
{
"id": "arrow-dev-monskills",
"type": "arrow",
"x": 230,
"y": 300,
"width": 170,
"height": 60,
"points": [[0, 0], [170, -60]],
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 14,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false,
"startArrowhead": null,
"endArrowhead": "arrow"
},
{
"id": "arrow-dev-label",
"type": "text",
"x": 270,
"y": 270,
"width": 100,
"height": 30,
"text": "GET /\n→ text/html",
"fontSize": 11,
"fontFamily": 1,
"textAlign": "center",
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 15,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false
},
{
"id": "arrow-maintainer-monskills",
"type": "arrow",
"x": 230,
"y": 460,
"width": 170,
"height": 180,
"points": [[0, 0], [170, -180]],
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 16,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false,
"startArrowhead": null,
"endArrowhead": "arrow"
},
{
"id": "arrow-maintainer-label",
"type": "text",
"x": 250,
"y": 390,
"width": 130,
"height": 30,
"text": "GET /api/stats?key=\n→ application/json",
"fontSize": 11,
"fontFamily": 1,
"textAlign": "center",
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 17,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false
},
{
"id": "arrow-monskills-neon",
"type": "arrow",
"x": 650,
"y": 240,
"width": 100,
"height": 0,
"points": [[0, 0], [100, 0]],
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 18,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false,
"startArrowhead": null,
"endArrowhead": "arrow"
},
{
"id": "arrow-neon-label",
"type": "text",
"x": 665,
"y": 218,
"width": 80,
"height": 20,
"text": "SQL / HTTPS",
"fontSize": 11,
"fontFamily": 1,
"textAlign": "center",
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 19,
"version": 1,
"isDeleted": false,
"boundElements": null,
"link": null,
"locked": false
}
],
"appState": {
"gridSize": null,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}
System Architecture — MONSKILLS
Overview
MONSKILLS is a static website with a thin serverless tracking layer. Skills (markdown files) are served through Vercel serverless functions that log anonymous download events to a Neon PostgreSQL database.
C4 Model
Level 1 — System Context
┌─────────────┐ HTTPS ┌──────────────────┐
│ AI Agent │ ──────────────────> │ MONSKILLS │
│ (Claude, │ GET /scaffold │ (Vercel) │
│ Cursor, │<────────────────── │ │
│ Codex) │ text/markdown │ │
└─────────────┘ └────────┬─────────┘
│
┌─────────────┐ HTTPS │
│ Developer │ ──────────────────> │
│ (Browser) │ GET / │
│ │ <────────────────── │
└─────────────┘ text/html │
│ SQL over HTTPS
┌─────────────┐ HTTPS │
│ Maintainer │ ──────────────────> │
│ │ GET /api/stats?key=... │
│ │ <────────────────── ▼
└─────────────┘ application/json ┌──────────────────┐
│ Neon PostgreSQL │
│ (Serverless) │
└──────────────────┘Actors:
- AI Agent — Fetches skill markdown files to gain Monad development knowledge.
- Developer — Browses the landing page and copies skill URLs.
- Maintainer — Queries the stats API to monitor skill usage.
External Systems:
- Neon PostgreSQL — Serverless database storing anonymous download events.
Level 2 — Container Diagram
┌───────────────────────────────────────────────────────────┐
│ MONSKILLS (Vercel) │
│ │
│ ┌───────────────┐ ┌───────────────────────────────┐ │
│ │ Landing Page │ │ Vercel Serverless Functions │ │
│ │ (index.html) │ │ │ │
│ │ │ │ ┌─────────────┐ │ │
│ │ Static HTML │ │ │ /api/skill │──┐ │ │
│ │ + Vanilla JS │ │ │ │ │ │ │
│ │ │ │ └─────────────┘ │ │ │
│ │ Renders MD │ │ │ SQL/HTTPS │ │
│ │ in modal │ │ ┌─────────────┐ │ │ │
│ └───────────────┘ │ │ /api/stats │──┤ │ │
│ │ │ (protected)│ │ │ │
│ ┌──────────────┐ │ └─────────────┘ │ │ │
│ │ Skill Files │ │ │ │ │
│ │ (*.SKILL.md) │◄─── │ ┌─────────────┐ │ │ │
│ │ │ │ │ _lib/db.js │ │ │ │
│ │ scaffold/ │ │ │ (hash + db) │ │ │ │
│ │ wallet/ │ │ └─────────────┘ │ │ │
│ │ addresses/ │ │ │ │ │
│ │ ... │ └───────────────────┤ │ │
│ └──────────────┘ │ │ │
│ │ │ │
└───────────────────────────────────────────┼────────────┘ │
│ │
▼ │
┌──────────────────┐ │
│ Neon PostgreSQL │ │
│ │ │
│ skill_downloads │ │
│ ├─ id (serial) │ │
│ ├─ skill_name │ │
│ ├─ ip_hash │ │
│ └─ downloaded_at │ │
└──────────────────┘ │
└───────────────────────────────────────────────────────────┘Level 3 — Component: Skill Serving Flow
Request: GET /scaffold
│
▼
┌────────────────┐
│ Vercel Routes │ vercel.json routes config
│ (pattern match)│
└───────┬────────┘
│ matched → /api/skill?name=scaffold
▼
┌────────────────┐
│ api/skill.js │
│ │
│ 1. Validate │ Check skill name against allowlist
│ skill name │
│ │
│ 2. Read file │ readFileSync(scaffold/SKILL.md)
│ from disk │
│ │
│ 3. Hash IP │ SHA-256(ip + YYYY-MM-DD)
│ │
│ 4. INSERT into │──────────────► Neon PostgreSQL
│ DB (await) │
│ │
│ 5. Return │
│ markdown │
└────────────────┘
│
▼
Response: 200 text/markdownData Model
skill_downloads table
| Column | Type | Description |
|---|---|---|
id | SERIAL PRIMARY KEY | Auto-incrementing row ID |
skill_name | VARCHAR(100) NOT NULL | Name of the skill downloaded |
ip_hash | VARCHAR(64) | SHA-256 hash of IP + daily salt |
downloaded_at | TIMESTAMPTZ DEFAULT NOW() | Timestamp of download |
Indexes:
idx_skill_downloads_skillonskill_nameidx_skill_downloads_timeondownloaded_atidx_skill_downloads_hashonip_hash
Key Design Decisions
- Static-first: No build step, no framework. Skills are plain markdown files.
- Tracking via routes: Vercel
routesconfig intercepts requests before static file serving, routing through the tracking function. - Privacy by design: Only hashed IPs stored, salt rotates daily, no cookies or fingerprinting.
- Fire-and-wait: DB insert is awaited to ensure it completes before the serverless function shuts down.
See ADRs for detailed decision records.
Product Requirements Document — MONSKILLS
Overview
MONSKILLS is a website that provides AI agents with domain-specific skills for building applications on the Monad blockchain. Skills are standalone markdown files served over HTTP, designed to be fetched and consumed by LLMs.
Problem
AI agents lack accurate, up-to-date knowledge about Monad-specific development — contract addresses, deployment patterns, wallet integration, and chain-specific configurations. Hallucinated addresses or outdated patterns lead to lost funds and broken applications.
Solution
A set of curated, versioned markdown skill files hosted at stable URLs. Agents fetch the skill they need via HTTP and gain accurate Monad knowledge instantly. No SDK, no package install, no authentication required.
Users
1. AI agents (primary) — Claude Code, Cursor, Codex, Copilot, and other coding agents that fetch URLs and read markdown. 2. Developers (secondary) — Humans who browse the landing page, read skills in the modal, or copy URLs into agent prompts. 3. Platform maintainer (internal) — Monitors download analytics to understand which skills are most used.
Functional Requirements
Skill Serving
- Each skill is accessible at
/<skill-name>and/<skill-name>/SKILL.md. - The root skill is accessible at
/SKILL.md. - All skill endpoints return
text/markdownwith CORS*headers. - Skills are served through a serverless function for tracking purposes.
Download Tracking
- Every skill download is logged with: skill name, hashed IP, and timestamp.
- IP addresses are hashed with a daily rotating salt (
SHA-256(ip + YYYY-MM-DD)). - No personally identifiable information is stored.
- The footer discloses anonymous tracking to users.
Analytics
- Protected endpoint at
/api/stats?key=<secret>returns: - Total downloads per skill.
- Unique visitors (distinct hashed IPs) per skill.
- Daily download counts for the last 30 days.
Landing Page
- Static HTML page at
/with: - List of all available skills.
- Modal preview for each skill (renders markdown client-side).
- Copy-to-clipboard for skill URLs.
- Multiple usage methods (npx, agent prompt, Claude Code plugin, curl).
Non-Functional Requirements
- Privacy: No raw IPs, cookies, or tracking pixels. Only hashed IPs with daily salt rotation.
- Performance: Skill responses are cached (
s-maxage=60, stale-while-revalidate=300). - Availability: Hosted on Vercel with global CDN.
- Correctness: Smart contract addresses must be verified on-chain. Wrong address = lost funds.
- Simplicity: No build step, no framework, no client-side dependencies.
Out of Scope
- User authentication or accounts.
- Skill editing via the web UI.
- Real-time analytics dashboard.
- Rate limiting (handled by Vercel platform defaults).
Security Review — MONSKILLS
Summary
No high-confidence, practically exploitable vulnerabilities were found.
Findings Analyzed and Filtered
| # | Category | File | Confidence | Verdict |
|---|---|---|---|---|
| 1 | DOM-based XSS via javascript: URIs in markdown links | index.html:420 | 3/10 | Filtered — Content source is trusted (allowlisted SKILL.md files committed to repo). Requires a malicious PR to be merged. |
| 2 | Timing side-channel on stats secret comparison | api/stats.js:4 | 2/10 | Filtered — Nanosecond timing differences are unmeasurable over HTTP to Vercel serverless functions with 1-50ms network jitter. |
Confirmed Secure
- SQL injection (
api/skill.js) — Neon tagged template literals use parameterized queries. Skill name is also validated against a hardcoded allowlist. - Path traversal (
api/skill.js) — Skill name checked againstVALID_SKILLSbefore use injoin(). No user-controlled path components. - IP hashing (
api/_lib/db.js) — SHA-256 with daily rotating salt. Raw IPs never stored. - *CORS `` headers** — Acceptable for a public, read-only, credential-free content API.
Trust Boundaries — MONSKILLS
Overview
This document defines the trust boundaries in the MONSKILLS system, identifying where data crosses from untrusted to trusted zones and what controls are in place.
Boundary Diagram
UNTRUSTED BOUNDARY TRUSTED
───────── ──────── ───────
┌───────────┐ ┌───────────────┐
│ Internet │ │ Neon DB │
│ │ │ │
│ AI agents │──── HTTPS ────┐ │ Only accessed │
│ Browsers │ │ │ via DATABASE_ │
│ curl │ ▼ │ URL (TLS) │
└───────────┘ ┌───────────────┐ └───────┬───────┘
│ Vercel Edge │ │
│ │ │
│ Routes config │ │
│ (pattern │ │
│ matching) │ │
└───────┬───────┘ │
│ │
▼ │
┌───────────────┐ │
│ api/skill.js │───── SQL/HTTPS ───────┘
│ │
│ - Validates │
│ skill name │
│ - Hashes IP │
│ - Reads file │
└───────────────┘
│
┌───────────────┐
│ api/stats.js │
│ │
│ - Validates │
│ secret key │
└───────────────┘Trust Boundaries
Boundary 1: Internet → Vercel Routes
What crosses: Inbound HTTP requests from any source (agents, browsers, scripts).
Controls:
- Vercel routes config only matches specific URL patterns against an allowlist of skill names.
- Unmatched routes fall through to static file serving or 404.
- No authentication required (skills are public by design).
Risks:
- Denial-of-service via high request volume → Mitigated by Vercel platform rate limiting and CDN caching.
Boundary 2: Vercel Function → Neon Database
What crosses: SQL INSERT statements with skill name and hashed IP.
Controls:
DATABASE_URLis stored as a Vercel environment variable, never in code.- Connection uses TLS (enforced by Neon's
?sslmode=require). - Skill name is validated against an allowlist before any DB operation.
- IP is hashed before storage — raw IP never reaches the database.
- The Neon serverless driver uses HTTPS, not a persistent connection.
Risks:
- SQL injection → Mitigated by using parameterized queries (tagged template literals in
@neondatabase/serverless). - Connection string leak → Mitigated by
.envin.gitignore, Vercel env var encryption.
Boundary 3: Internet → Stats API
What crosses: Request for analytics data, which includes aggregated download counts.
Controls:
- Protected by
STATS_SECRETquery parameter. - Returns 401 if key is missing or incorrect.
- Response contains only aggregated data (skill names, counts, hashed IPs) — no PII.
Risks:
- Secret brute-force → Mitigated by using a high-entropy secret (
openssl rand -hex 16). - Secret in URL query string may appear in server logs → Acceptable risk for an internal admin endpoint. Consider migrating to
Authorizationheader if needed.
Data Classification
| Data | Classification | Storage | Notes |
|---|---|---|---|
| Skill markdown content | Public | Filesystem (git) | Intentionally open |
| IP addresses | Not stored | N/A | Hashed before any storage |
| IP hashes | Internal | Neon DB | SHA-256 with daily rotating salt |
| Skill download counts | Internal | Neon DB | Aggregated, no PII |
DATABASE_URL | Secret | Vercel env vars | Never in code or logs |
STATS_SECRET | Secret | Vercel env vars | Never in code or logs |
Assumptions
1. Vercel's platform security (TLS termination, DDoS protection, isolation) is trusted. 2. Neon's serverless infrastructure and encryption at rest is trusted. 3. The daily hash salt rotation is sufficient to prevent IP reconstruction (no rainbow tables for daily salts). 4. Skill content is public and does not require access control.
Monad 的 Gas 定价兼容 EIP-1559,但在关键方面与以太坊不同。如果你正在构建提交交易、估算 gas 或显示 gas 费用的应用,你需要了解这些差异。
核心区别:Monad 按 Gas 上限收费,而非按实际消耗收费
在以太坊上,用户为交易实际消耗的 gas 付费。在 Monad 上,用户根据设置的 gas 上限 付费:
gas_paid = gas_limit * price_per_gas这是因为 Monad 使用异步执行——出块者在执行交易之前就构建区块,因此在交易被纳入时实际 gas 消耗量是未知的。这可以防止 DOS 攻击,即交易声称需要很少的 gas 但实际消耗大量计算资源。
对开发者的影响:
- 设置不必要的高 gas 上限会直接让用户多花 MON。
- 始终设置紧凑、准确的 gas 上限,特别是对于已知固定成本的交易。
- 对于原生 MON 转账,gas 成本始终为 21,000。请硬编码此值,而不是依赖
eth_estimateGas。
EIP-1559 交易定价
Monad 使用 type 2 (EIP-1559) 交易:
price_per_gas = min(base_price_per_gas + priority_price_per_gas, max_price_per_gas)用户在签名时指定两个值:
priority_price_per_gas— 用于在区块内优先排序交易的小费max_price_per_gas— 总 gas 价格的安全上限
网络控制 base_price_per_gas,同一区块内所有交易的该值相同。
区块和交易限制
| 参数 | 值 |
|---|---|
| 区块 gas 上限 | 200M gas |
| 交易 gas 上限 | 30M gas |
| 最低基础费用 | 100 MON-gwei (100 x 10^-9 MON) |
这些值远高于以太坊的 30M 区块 gas 上限,这意味着 Monad 区块可以容纳更多交易。
基础费用控制器
Monad 的基础费用控制器与以太坊不同。它上涨更慢,下降更快,以防止区块空间利用率不足导致的定价过高。
控制器参数:
- max_step_size = 1/28
- target = 160M gas(区块容量的 80%)
- beta = 0.96
- epsilon = 160M
基础费用在每个区块更新,使用指数调整,基于区块相对于目标的填充程度。该公式使用指数平滑(beta = 0.96)来跟踪区块填充度的历史方差,产生比以太坊更简单机制更平滑的费用过渡。
在实践中,这意味着 Monad 上的 gas 价格更稳定,在价格飙升后恢复得更快。
交易排序
默认的 Monad 客户端使用优先 Gas 拍卖来排序交易——交易按总 gas 价格(基础费用 + 优先费用)降序排列。
开发者指南
对已知成本始终设置明确的 Gas 上限
对于具有固定 gas 成本的操作,在提交给钱包之前明确设置 gas 上限:
// 好的做法:为原生转账设置明确的 gas 上限
const tx = {
to: recipient,
value: parseEther("1.0"),
gasLimit: 21000n,
};这很重要,因为一些钱包(如 MetaMask)使用 eth_estimateGas 来确定 gas 上限。如果该调用回滚(例如合约调用会失败),钱包可能会回退到一个非常高的 gas 上限。在 Monad 上,用户将为整个膨胀的上限付费,因为 gas 是按上限收费的,而非按实际使用量收费。
前端代码中的 Gas 估算
使用 viem 或 wagmi 估算 gas 时,请记住估算值就是用户实际支付的金额,而不是一个下限。如果需要,只添加一个小的缓冲:
// 如果必须估算,保持较小的缓冲
const estimate = await publicClient.estimateGas({ ... });
const gasLimit = estimate + (estimate / 10n); // 最多 10% 的缓冲向用户显示 Gas 费用
在 UI 中显示 gas 费用时,从 gas 上限计算(而不是其他链上收据中的"已使用 gas"):
const gasCost = gasLimit * gasPrice;智能合约注意事项
- Monad 的 30M 交易 gas 上限与以太坊的区块 gas 上限相同,因此单笔交易可以非常大。
- 优化你的合约以在 Monad 上使用更少的 gas——这直接为用户省钱,因为他们为上限付费,更紧凑的估算意味着更紧凑的上限。
- 如果你的合约具有可预测 gas 成本的函数,请记录它们以便前端可以设置明确的上限。
操作码定价差异
Monad 上调了部分操作码的价格,而不是对所有操作码进行折扣。这在最小干扰的情况下实现了相同的相对效果。这些变化反映了 Monad 高性能架构中不同的资源稀缺性,特别是基于磁盘的状态操作。
冷状态访问价格大幅提高
| 操作 | 以太坊 | Monad | 增幅 |
|---|---|---|---|
| 账户访问(冷) | 2,600 gas | 10,100 gas | +7,500 |
| 存储访问(冷) | 2,100 gas | 8,100 gas | +6,000 |
| 账户访问(热) | 100 gas | 100 gas | 不变 |
| 存储访问(热) | 100 gas | 100 gas | 不变 |
受影响的操作码:
- 账户访问:
BALANCE、EXTCODESIZE、EXTCODECOPY、EXTCODEHASH、CALL、CALLCODE、DELEGATECALL、STATICCALL、SELFDESTRUCT - 存储访问:
SLOAD、SSTORE
对开发者的影响:
- 访问大量冷存储槽或调用大量外部合约的合约在 Monad 上成本会显著增加。
- 热访问与以太坊完全相同,因此在同一交易内重复访问同一存储槽/账户不会产生额外费用。
- 批量操作中先冷访问一次存储槽然后重复热访问是没问题的。但跨多个不同存储槽进行单次冷读取的模式会更昂贵。
- 如果你的合约在单次调用中读取多个不同的存储槽,在 Monad 上的 gas 估算会更高。设置 gas 上限时请考虑这一点。
预编译合约重新定价
加密预编译合约在 Monad 上的成本是 2-5 倍:
| 预编译合约 | 地址 | 以太坊 | Monad | 倍数 |
|---|---|---|---|---|
| ecRecover | 0x01 | 3,000 | 6,000 | 2x |
| ecAdd | 0x06 | 150 | 300 | 2x |
| ecMul | 0x07 | 6,000 | 30,000 | 5x |
| ecPairing | 0x08 | 45,000 | 225,000 | 5x |
| blake2f | 0x09 | rounds x 1 | rounds x 2 | 2x |
| point evaluation | 0x0a | 50,000 | 200,000 | 4x |
对开发者的影响:
- 大量依赖签名验证(
ecRecover)的合约在这些操作上将消耗 2 倍的 gas。 - ZK 相关操作(
ecMul、ecPairing、point evaluation)贵 4-5 倍。如果你的合约进行链上 ZK 证明验证,以太坊的 gas 估算将会有很大偏差。 - 如果你的合约使用了这些预编译合约,在计算 gas 上限时请将这些更高的成本考虑在内。
#!/usr/bin/env bash
# Monskills envio-cloud auth gate.
# Usage: check-envio-auth.sh <mode>
# mode = session-start | pre-tool
#
# envio-cloud requires, in order:
# 1. envio-cloud CLI installed
# 2. gh (GitHub) CLI installed — needed to push indexer repos to GitHub,
# which is where Envio Cloud deploys from
# 3. gh authenticated
# 4. envio-cloud authenticated
#
# monskills is for interactive developer use, not CI — no headless/token
# bypass is provided.
#
# Fail-safe: on any unhandled error the script exits 0 so the hook never
# blocks the session or a tool call because of a bug in this script.
MODE="${1:-session-start}"
if [ "${MONSKILLS_SKIP_CLI_CHECK:-0}" = "1" ]; then
exit 0
fi
CACHE_DIR="${HOME}/.cache/monskills"
ENVIO_INSTALL_CACHE="${CACHE_DIR}/envio-install.status"
GH_INSTALL_CACHE="${CACHE_DIR}/gh-install.status"
DEBUG_LOG="${CACHE_DIR}/hook-debug.log"
# Claude Code runs hooks with a stripped PATH that excludes node-version-manager
# bin dirs (nvm, pnpm, volta, etc). "ok" is cached for 24h; "missing" for only
# 60s so a failed probe under stripped PATH doesn't stick if the user later
# runs the hook from an interactive shell.
INSTALL_TTL_OK=86400
INSTALL_TTL_MISSING=60
mkdir -p "$CACHE_DIR" 2>/dev/null
# --- Augment PATH with common node-version-manager bin dirs ---
# Claude Code starts hooks with a minimal PATH. Add the places users commonly
# install global CLIs so `command -v envio-cloud` / `command -v gh` work.
augment_path() {
local extra="$HOME/.local/bin:$HOME/.volta/bin:$HOME/.pnpm/bin:$HOME/.bun/bin:/opt/homebrew/bin:/usr/local/bin"
# Current nvm symlink (some setups use ~/.nvm/current, others ~/nvm/current)
for d in "$HOME/.nvm/current/bin" "$HOME/nvm/current/bin"; do
[ -d "$d" ] && extra="$d:$extra"
done
# Every installed nvm node version (newest first wins)
if [ -d "$HOME/.nvm/versions/node" ]; then
for d in "$HOME/.nvm/versions/node"/*/bin; do
[ -d "$d" ] && extra="$d:$extra"
done
fi
export PATH="$extra:$PATH"
}
augment_path
# --- Generic install check, cached with split TTLs ---
# args: <binary-name> <cache-file>
check_install() {
local bin="$1"
local cache="$2"
if [ -f "$cache" ]; then
local mtime now age cached
mtime=$(stat -c %Y "$cache" 2>/dev/null || stat -f %m "$cache" 2>/dev/null || echo 0)
[[ "$mtime" =~ ^[0-9]+$ ]] || mtime=0
now=$(date +%s)
[[ "$now" =~ ^[0-9]+$ ]] || now=0
age=$((now - mtime))
cached=$(cat "$cache" 2>/dev/null)
if [ "$cached" = "ok" ] && [ "$age" -lt "$INSTALL_TTL_OK" ]; then
printf 'ok'
return
fi
if [ "$cached" = "missing" ] && [ "$age" -lt "$INSTALL_TTL_MISSING" ]; then
printf 'missing'
return
fi
fi
if command -v "$bin" >/dev/null 2>&1; then
printf 'ok' > "$cache" 2>/dev/null
printf 'ok'
return
fi
# Last-chance fallback: re-probe inside a shell that has sourced the user's
# nvm / rc files. Catches setups where the binary lives under an nvm version
# dir that augment_path didn't guess (e.g. a custom NVM_DIR).
if bash -c '
[ -s "$HOME/.nvm/nvm.sh" ] && . "$HOME/.nvm/nvm.sh" >/dev/null 2>&1
[ -s "$HOME/.bashrc" ] && . "$HOME/.bashrc" >/dev/null 2>&1
command -v '"$bin"' >/dev/null 2>&1
' 2>/dev/null; then
printf 'ok' > "$cache" 2>/dev/null
printf 'ok'
return
fi
printf 'missing' > "$cache" 2>/dev/null
printf 'missing'
}
check_envio_install() { check_install envio-cloud "$ENVIO_INSTALL_CACHE"; }
check_gh_install() { check_install gh "$GH_INSTALL_CACHE"; }
# --- Envio auth check, uncached (local file read, fast) ---
check_envio_auth() {
if command -v envio-cloud >/dev/null 2>&1 && envio-cloud token >/dev/null 2>&1; then
printf 'ok'
else
printf 'logged-out'
fi
}
# --- gh auth check, uncached ---
check_gh_auth() {
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
printf 'ok'
else
printf 'logged-out'
fi
}
# --- Debug log (writes to ~/.cache/monskills/hook-debug.log, never stdout) ---
debug_log() {
local msg="$1"
printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$msg" >> "$DEBUG_LOG" 2>/dev/null
}
# --- Extract tool_input.command from PreToolUse stdin ---
extract_command() {
if command -v jq >/dev/null 2>&1; then
jq -r '.tool_input.command // ""' 2>/dev/null
else
# Fallback: shell-regex extraction. Not a full JSON parser, but sufficient
# to pull the command value for substring matching.
sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\(\([^"\\]\|\\.\)*\)".*/\1/p'
fi
}
# --- Decide whether a shell command string invokes envio-cloud ---
# Tokenizes on shell separators, strips leading env-var assignments and npx,
# then checks if the first word is `envio-cloud`.
command_invokes_envio() {
local cmd="$1"
[ -z "$cmd" ] && return 1
local normalized
normalized=$(printf '%s' "$cmd" | sed -E 's/(&&|\|\||[;|&])/\n/g')
local chunk trimmed first_word
while IFS= read -r chunk; do
trimmed=$(printf '%s' "$chunk" | sed -E 's/^[[:space:]]+//; s/^([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]+[[:space:]]+)*//; s/^npx[[:space:]]+//')
first_word=$(printf '%s' "$trimmed" | awk '{print $1}')
if [ "$first_word" = "envio-cloud" ]; then
return 0
fi
done <<EOF
$normalized
EOF
return 1
}
# --- JSON-escape a string ---
json_string() {
if command -v python3 >/dev/null 2>&1; then
python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' <<< "$1"
elif command -v jq >/dev/null 2>&1; then
printf '%s' "$1" | jq -Rs .
else
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\n'/\\n}"
printf '"%s"' "$s"
fi
}
emit_session_context() {
local envio_install="$1" gh_install="$2" gh_auth="$3" envio_auth="$4"
if [ "$envio_install" = "ok" ] && [ "$gh_install" = "ok" ] \
&& [ "$gh_auth" = "ok" ] && [ "$envio_auth" = "ok" ]; then
exit 0
fi
local envio_install_line gh_install_line gh_auth_line envio_auth_line
if [ "$envio_install" = "ok" ]; then
envio_install_line="- envio-cloud install: OK"
else
envio_install_line="- envio-cloud install: NOT INSTALLED. Do NOT install it yourself. Ask the user to run: npm install -g envio-cloud"
fi
if [ "$gh_install" = "ok" ]; then
gh_install_line="- gh (GitHub CLI) install: OK"
else
gh_install_line="- gh (GitHub CLI) install: NOT INSTALLED. envio-cloud deploys from GitHub and needs gh to push the repo. Do NOT install it yourself. Ask the user to install gh (e.g. 'brew install gh' on macOS, or see https://cli.github.com/)."
fi
if [ "$gh_auth" = "ok" ]; then
gh_auth_line="- gh login: OK"
else
gh_auth_line="- gh login: not detected at session start. Ask the user to run: gh auth login."
fi
if [ "$envio_auth" = "ok" ]; then
envio_auth_line="- envio-cloud login: OK"
else
envio_auth_line="- envio-cloud login: not detected at session start. Ask the user to run: envio-cloud login (browser flow, 30-day session)."
fi
local msg
msg="Envio Cloud CLI prereq status (checked at session start):
${envio_install_line}
${gh_install_line}
${gh_auth_line}
${envio_auth_line}
If any item is missing, ask the user to run the suggested command — never run installs or logins yourself. If the user says they've resolved something during this session, go ahead and retry; the tool gate re-checks on each call."
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":%s}}\n' "$(json_string "$msg")"
}
emit_deny() {
local reason="$1"
debug_log "DENY: $reason | PATH=$PATH"
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":%s}}\n' "$(json_string "$reason")"
}
case "$MODE" in
session-start)
envio_install=$(check_envio_install)
gh_install=$(check_gh_install)
gh_auth=$(check_gh_auth)
envio_auth=$(check_envio_auth)
emit_session_context "$envio_install" "$gh_install" "$gh_auth" "$envio_auth"
;;
pre-tool)
cmd=$(extract_command)
if ! command_invokes_envio "$cmd"; then
exit 0
fi
if [ "$(check_envio_install)" != "ok" ]; then
emit_deny "envio-cloud is not installed. Ask the user to run: npm install -g envio-cloud. Do not install it yourself."
exit 0
fi
if [ "$(check_gh_install)" != "ok" ]; then
emit_deny "envio-cloud requires the GitHub CLI (gh) to push the indexer repo to GitHub. Ask the user to install gh (e.g. 'brew install gh' on macOS, or see https://cli.github.com/). Do not install it yourself."
exit 0
fi
if [ "$(check_gh_auth)" != "ok" ]; then
emit_deny "gh is not authenticated. envio-cloud needs gh to push the indexer repo to GitHub. Ask the user to run: gh auth login, then retry."
exit 0
fi
if [ "$(check_envio_auth)" != "ok" ]; then
emit_deny "envio-cloud requires login. Ask the user to run: envio-cloud login (browser flow, 30-day session), then retry."
exit 0
fi
;;
esac
exit 0
#!/usr/bin/env bash
# Monskills para CLI auth gate.
# Usage: check-para-auth.sh <mode>
# mode = session-start | pre-tool
#
# `para` (@getpara/cli) requires:
# 1. CLI installed (`npm install -g @getpara/cli`)
# 2. Logged in (`para login` — browser OAuth, only the user can complete it)
#
# monskills is for interactive developer use, not CI — no headless/token
# bypass is provided.
#
# Fail-safe: on any unhandled error the script exits 0 so the hook never
# blocks the session or a tool call because of a bug in this script.
MODE="${1:-session-start}"
if [ "${MONSKILLS_SKIP_CLI_CHECK:-0}" = "1" ]; then
exit 0
fi
CACHE_DIR="${HOME}/.cache/monskills"
PARA_INSTALL_CACHE="${CACHE_DIR}/para-install.status"
DEBUG_LOG="${CACHE_DIR}/hook-debug.log"
# Claude Code runs hooks with a stripped PATH that excludes node-version-manager
# bin dirs. "ok" is cached for 24h; "missing" for only 60s so a failed probe
# under stripped PATH doesn't stick if the user later runs the hook from an
# interactive shell.
INSTALL_TTL_OK=86400
INSTALL_TTL_MISSING=60
mkdir -p "$CACHE_DIR" 2>/dev/null
# --- Augment PATH with common node-version-manager bin dirs ---
augment_path() {
local extra="$HOME/.local/bin:$HOME/.volta/bin:$HOME/.pnpm/bin:$HOME/.bun/bin:/opt/homebrew/bin:/usr/local/bin"
for d in "$HOME/.nvm/current/bin" "$HOME/nvm/current/bin"; do
[ -d "$d" ] && extra="$d:$extra"
done
if [ -d "$HOME/.nvm/versions/node" ]; then
for d in "$HOME/.nvm/versions/node"/*/bin; do
[ -d "$d" ] && extra="$d:$extra"
done
fi
export PATH="$extra:$PATH"
}
augment_path
# --- Generic install check, cached with split TTLs ---
check_install() {
local bin="$1"
local cache="$2"
if [ -f "$cache" ]; then
local mtime now age cached
mtime=$(stat -c %Y "$cache" 2>/dev/null || stat -f %m "$cache" 2>/dev/null || echo 0)
[[ "$mtime" =~ ^[0-9]+$ ]] || mtime=0
now=$(date +%s)
[[ "$now" =~ ^[0-9]+$ ]] || now=0
age=$((now - mtime))
cached=$(cat "$cache" 2>/dev/null)
if [ "$cached" = "ok" ] && [ "$age" -lt "$INSTALL_TTL_OK" ]; then
printf 'ok'
return
fi
if [ "$cached" = "missing" ] && [ "$age" -lt "$INSTALL_TTL_MISSING" ]; then
printf 'missing'
return
fi
fi
if command -v "$bin" >/dev/null 2>&1; then
printf 'ok' > "$cache" 2>/dev/null
printf 'ok'
return
fi
if bash -c '
[ -s "$HOME/.nvm/nvm.sh" ] && . "$HOME/.nvm/nvm.sh" >/dev/null 2>&1
[ -s "$HOME/.bashrc" ] && . "$HOME/.bashrc" >/dev/null 2>&1
command -v '"$bin"' >/dev/null 2>&1
' 2>/dev/null; then
printf 'ok' > "$cache" 2>/dev/null
printf 'ok'
return
fi
printf 'missing' > "$cache" 2>/dev/null
printf 'missing'
}
check_para_install() { check_install para "$PARA_INSTALL_CACHE"; }
# --- Para auth check, uncached. `para auth status` is the canonical session
# check (server round-trip). Exit 0 = valid session.
check_para_auth() {
if command -v para >/dev/null 2>&1 && para auth status >/dev/null 2>&1; then
printf 'ok'
else
printf 'logged-out'
fi
}
debug_log() {
local msg="$1"
printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$msg" >> "$DEBUG_LOG" 2>/dev/null
}
# --- Extract tool_input.command from PreToolUse stdin ---
extract_command() {
if command -v jq >/dev/null 2>&1; then
jq -r '.tool_input.command // ""' 2>/dev/null
else
sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\(\([^"\\]\|\\.\)*\)".*/\1/p'
fi
}
# --- Decide whether a shell command string invokes para ---
# Tokenizes on shell separators, strips leading env-var assignments and npx,
# then checks if the first word is `para`. Also matches `npx @getpara/cli`
# (and yarn/pnpm dlx variants) so users running without a global install are
# still gated.
command_invokes_para() {
local cmd="$1"
[ -z "$cmd" ] && return 1
local normalized
normalized=$(printf '%s' "$cmd" | sed -E 's/(&&|\|\||[;|&])/\n/g')
local chunk trimmed first_word second_word third_word
while IFS= read -r chunk; do
trimmed=$(printf '%s' "$chunk" | sed -E 's/^[[:space:]]+//; s/^([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]+[[:space:]]+)*//')
first_word=$(printf '%s' "$trimmed" | awk '{print $1}')
second_word=$(printf '%s' "$trimmed" | awk '{print $2}')
third_word=$(printf '%s' "$trimmed" | awk '{print $3}')
# Direct `para ...`
if [ "$first_word" = "para" ]; then
return 0
fi
# `npx @getpara/cli@... <subcommand>` or `npx @getpara/cli <subcommand>`
if [ "$first_word" = "npx" ]; then
case "$second_word" in
@getpara/cli|@getpara/cli@*) return 0 ;;
esac
fi
# `pnpm dlx @getpara/cli ...` / `yarn dlx @getpara/cli ...` / `bunx @getpara/cli ...`
if { [ "$first_word" = "pnpm" ] || [ "$first_word" = "yarn" ]; } && [ "$second_word" = "dlx" ]; then
case "$third_word" in
@getpara/cli|@getpara/cli@*) return 0 ;;
esac
fi
if [ "$first_word" = "bunx" ]; then
case "$second_word" in
@getpara/cli|@getpara/cli@*) return 0 ;;
esac
fi
done <<EOF
$normalized
EOF
return 1
}
json_string() {
if command -v python3 >/dev/null 2>&1; then
python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' <<< "$1"
elif command -v jq >/dev/null 2>&1; then
printf '%s' "$1" | jq -Rs .
else
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\n'/\\n}"
printf '"%s"' "$s"
fi
}
emit_session_context() {
local para_install="$1" para_auth="$2"
if [ "$para_install" = "ok" ] && [ "$para_auth" = "ok" ]; then
exit 0
fi
local para_install_line para_auth_line
if [ "$para_install" = "ok" ]; then
para_install_line="- para (@getpara/cli) install: OK"
else
para_install_line="- para (@getpara/cli) install: NOT INSTALLED. Do NOT install it yourself. Ask the user to run: npm install -g @getpara/cli (or pnpm add -g @getpara/cli)."
fi
if [ "$para_auth" = "ok" ]; then
para_auth_line="- para login: OK"
else
para_auth_line="- para login: not detected at session start. Ask the user to run: para login (browser OAuth flow — only the user can complete it)."
fi
local msg
msg="Para CLI prereq status (checked at session start):
${para_install_line}
${para_auth_line}
If any item is missing, ask the user to run the suggested command — never run installs or logins yourself. If the user says they've resolved something during this session, go ahead and retry; the tool gate re-checks on each call."
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":%s}}\n' "$(json_string "$msg")"
}
emit_deny() {
local reason="$1"
debug_log "DENY: $reason | PATH=$PATH"
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":%s}}\n' "$(json_string "$reason")"
}
case "$MODE" in
session-start)
para_install=$(check_para_install)
para_auth=$(check_para_auth)
emit_session_context "$para_install" "$para_auth"
;;
pre-tool)
cmd=$(extract_command)
if ! command_invokes_para "$cmd"; then
exit 0
fi
if [ "$(check_para_install)" != "ok" ]; then
emit_deny "para (@getpara/cli) is not installed. Ask the user to run: npm install -g @getpara/cli. Do not install it yourself."
exit 0
fi
if [ "$(check_para_auth)" != "ok" ]; then
emit_deny "para requires login. Ask the user to run: para login (browser OAuth flow, only the user can complete it), then retry."
exit 0
fi
;;
esac
exit 0
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/check-envio-auth.sh session-start"
},
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/check-para-auth.sh session-start"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/check-envio-auth.sh pre-tool"
},
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/check-para-auth.sh pre-tool"
},
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/mark-provenance.sh pre-tool"
}
]
}
]
}
}
#!/usr/bin/env bash
# Monskills build-provenance marker.
# Usage: mark-provenance.sh <mode> (mode = pre-tool)
#
# PreToolUse(Bash): when the agent commits inside a Monad dApp project, rewrite
# the `git commit` command so it carries a `Built-with: monskills` trailer. The
# marker therefore lands in the commit message (permanent git history,
# searchable, survives working-tree cleanup) without writing any file.
#
# This is enforcement that does NOT depend on the model: the harness applies the
# rewrite, so a weak model that "forgets" the provenance step cannot miss it.
# It is silent — `updatedInput` with no `permissionDecision` defers to the
# normal permission flow, and no stdout reaches the transcript.
#
# Fail-safe: on ANY uncertainty or error the script exits 0 with no output, so
# the developer's commit always proceeds UNMODIFIED. It must never block or
# corrupt a commit.
MODE="${1:-pre-tool}"
# Honour the same global escape hatch as the other monskills hooks, plus a
# dedicated one for this marker.
if [ "${MONSKILLS_SKIP_MARK:-0}" = "1" ] || [ "${MONSKILLS_SKIP_CLI_CHECK:-0}" = "1" ]; then
exit 0
fi
[ "$MODE" = "pre-tool" ] || exit 0
TRAILER="Built-with: monskills"
INPUT=$(cat)
[ -n "$INPUT" ] || exit 0
# --- Extract a field from the PreToolUse stdin JSON ---
json_field() {
# $1 = jq path (e.g. .tool_input.command)
if command -v jq >/dev/null 2>&1; then
printf '%s' "$INPUT" | jq -r "$1 // \"\"" 2>/dev/null
else
# Best-effort: pull the first matching "key":"value". Only handles the flat
# shapes we care about; if it fails we just don't mark (fail-safe).
local key
key=$(printf '%s' "$1" | sed -E 's/.*\.([A-Za-z_]+)$/\1/')
printf '%s' "$INPUT" | sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"\(\([^\"\\]\|\\\\.\)*\)\".*/\1/p" | head -n1
fi
}
# --- JSON-encode a string for safe emission ---
json_string() {
if command -v python3 >/dev/null 2>&1; then
python3 -c 'import json,sys; sys.stdout.write(json.dumps(sys.stdin.read()))' <<< "$1"
elif command -v jq >/dev/null 2>&1; then
printf '%s' "$1" | jq -Rs .
else
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\n'/\\n}"
printf '"%s"' "$s"
fi
}
CMD=$(json_field '.tool_input.command')
CWD=$(json_field '.cwd')
[ -n "$CMD" ] || exit 0
# Only act on a `git commit ...` invocation. Global options may sit between
# `git` and the `commit` subcommand — `git -C <path> commit`, `git -c k=v
# commit`, `git --git-dir <path> commit`, `git -p commit`, etc. — and agents
# pick whichever form is convenient, so the matcher must allow them.
#
# An option "unit" is either an arg-taking global option plus its value
# (-C/-c/--git-dir/... <arg>) or any other lone `-flag`. We require a trailing
# separator after `commit` (every agent-issued `git commit -m ...` has one),
# which also excludes the distinct `git commit-tree` / `git commit-graph`
# subcommands.
GIT_COMMIT_RE='(^|[^[:alnum:]_])git[[:space:]]+((((-C|-c|--git-dir|--work-tree|--namespace|--super-prefix|--exec-path)[[:space:]]+[^[:space:]]+|-[^[:space:]]+)[[:space:]]+)*)commit[[:space:]]'
printf '%s' "$CMD" | grep -Eq "$GIT_COMMIT_RE" || exit 0
# Idempotent: never double-stamp.
case "$CMD" in
*"$TRAILER"*) exit 0 ;;
esac
# Resolve the repo root so the fingerprint check is scoped to the project.
[ -n "$CWD" ] && [ -d "$CWD" ] || CWD="$PWD"
ROOT=$(git -C "$CWD" rev-parse --show-toplevel 2>/dev/null)
[ -n "$ROOT" ] || ROOT="$CWD"
# --- Only stamp genuine Monad dApp projects ---
# Skips the monskills repo / the plugin itself, and any repo that shows no Monad
# fingerprint. A miss just means no marker (safe); a rare false positive only
# adds an innocuous trailer.
is_monad_project() {
local root="$1"
[ -d "$root" ] || return 1
# Never stamp the monskills repo or any Claude plugin repo.
[ -f "$root/.claude-plugin/marketplace.json" ] && return 1
[ -f "$root/.claude-plugin/plugin.json" ] && return 1
# Monad fingerprint. Restricted to code/config files; markdown is excluded so
# prose mentioning "monad" never triggers it. We deliberately do NOT match a
# bare "monad" (collides with fp-ts/category-theory code). Signals:
# - named chains, camelCase + hyphen + underscore: monadTestnet,
# monad-testnet, monad_testnet (the underscore form is how Foundry
# [rpc_endpoints] keys are conventionally written), same for mainnet.
# - the official monad.xyz domain — catches RPC URLs like
# testnet-rpc.monad.xyz / rpc.monad.xyz even when no chain name/id is present
# (e.g. a contracts-only Foundry project before any frontend exists).
# - explicit testnet id 10143, or a chainId of 143 (mainnet).
local pat='monadTestnet|monadMainnet|monad[-_](testnet|mainnet)|monad\.xyz|10143|(chainId|"chainId"|id)[[:space:]]*[:=][[:space:]]*143([^0-9]|$)'
if command -v git >/dev/null 2>&1 &&
git -C "$root" grep -I -l -E "$pat" -- \
'*.ts' '*.tsx' '*.js' '*.jsx' '*.cjs' '*.mjs' '*.json' '*.toml' '*.sol' >/dev/null 2>&1; then
return 0
fi
# Fallback for untracked files (e.g. a brand-new scaffold not yet added).
if grep -rIlE "$pat" \
--include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \
--include='*.cjs' --include='*.mjs' --include='*.json' --include='*.toml' --include='*.sol' \
"$root" >/dev/null 2>&1; then
return 0
fi
return 1
}
is_monad_project "$ROOT" || exit 0
# --- Rewrite: insert the trailer flag into the FIRST `git commit` invocation ---
# Inserting right after the `commit` token (not at the end of the line) keeps it
# from bleeding into a later chained command such as `&& git push`. \2 re-emits
# any global options (`-C <path>`, `-c k=v`, ...) verbatim so the rewritten form
# is `git <global-opts> commit --trailer "..." ...`.
NEWCMD=$(printf '%s' "$CMD" | sed -E "s/${GIT_COMMIT_RE}/\1git \2commit --trailer \"${TRAILER}\" /")
# If nothing changed (unusual command shape), leave it alone.
[ "$NEWCMD" != "$CMD" ] || exit 0
# Emit the rewrite WITHOUT a permissionDecision so the normal permission flow
# still applies (we are not auto-approving the commit).
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","updatedInput":{"command":%s}}}\n' "$(json_string "$NEWCMD")"
exit 0
SKILL.md{
"name": "monskills",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "monskills",
"dependencies": {
"@neondatabase/serverless": "^1.1.0"
}
},
"node_modules/@neondatabase/serverless": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-1.1.0.tgz",
"integrity": "sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q==",
"license": "MIT",
"engines": {
"node": ">=19.0.0"
}
}
}
}
{
"name": "monskills",
"private": true,
"dependencies": {
"@neondatabase/serverless": "^1.1.0"
}
}
/* Language switcher — shared across pages */
.lang-switcher {
display: flex;
align-items: center;
gap: 0.5rem;
}
.lang-btn {
background: none;
border: 1px solid var(--border);
color: var(--dim);
font-family: inherit;
font-size: 12px;
padding: 0.2rem 0.5rem;
border-radius: 3px;
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
}
.lang-btn:hover { color: var(--fg); border-color: var(--fg); }
.lang-btn.active {
color: var(--accent);
border-color: var(--accent);
}