T09 · Insecure Skill Coding Practices
- Location
actions/wallet.js:384- Finding
Generated Wallet Private Key and Mnemonic Are Exposed Through Process Output
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This crypto-wallet skill is purpose-built for on-chain operations, but it exposes fund-moving actions and wallet secrets to AI-connected workflows without strong enough safety boundaries.
Review before installing. Use only a dedicated low-value wallet, do not put a main wallet private key or mnemonic in an agent-accessible .env or MCP config, avoid enabling MCP transaction tools unless you add your own confirmation and spending limits, and treat create-wallet output as immediately sensitive because it prints recovery material.
actions/wallet.js:384Generated Wallet Private Key and Mnemonic Are Exposed Through Process Output
mcp-server.js:239MCP Tools Can Sign and Broadcast Asset Transfers and Arbitrary Transactions Without Mandatory Human Confirmation
actions/swapBridge.js:17Unlimited ERC-20 Allowances Are Granted to Spenders Selected by Remote Quote Services
actions/swapBridge.js:185Direct Uniswap and PancakeSwap Routes Set Minimum Output to Zero
actions/common.js:399Model-Controlled Custom RPC URLs Permit Server-Side Requests to Arbitrary Endpoints
bin/install.js:71Installer Resolves Mutable Dependencies Without a Lockfile and Recommends Unversioned Remote Execution
The README asserts explicit human approval for any on-chain transaction, but later describes MCP/agent-exposed tools like transfer, swap, bridge, mint_nft, and custom_tx without documenting an enforced confirmation gate. In an LLM-connected execution environment, this mismatch can cause operators to overtrust the tool and permit autonomous fund-moving actions.
The README makes a security claim that the skill never stores or accesses mnemonics/seed phrases during operations, yet elsewhere documents wallet generation that writes mnemonic material to .env. This is dangerous because users may rely on a false security boundary and expose recovery secrets in plaintext local storage.
The README instructs users to store a wallet private key in a .env file for an AI-integrated skill. This creates a direct path for the runtime that interprets natural language and executes wallet operations to access high-value credentials, undermining the claimed zero-knowledge separation.
YARA rule matched a hack tool or exploit indicator (offensive tools, reconnaissance, privilege escalation, or exploit frameworks).
``
Mint an NFT on Base by calling the claim(address,uint256) function with arguments:
node index.js mint --chain base --contract 0x123456789... --function "claim(address,uint256)" --args '["0xYourAddress", 1]' --value 0.00075
Send custom hex data to a smart contract:
node index.js custom --chain base --to 0xTargetAddress --data 0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000
Below are standard interaction cycles showing how an AI Agent (e.g. OpenClaw) leverages the web3-ops skill under the hood to fulfill user requests safely and present natural-language responses.
When a user asks to inspect their balance, the agent resolves the chain name and runs a read-only query.
User 💬: "How much ETH do I have on Base?"
Age
The agent integration section tells users to place the wallet private key in the agent project's main .env, making the secret available to the broader agent environment rather than a tightly isolated signing component. In context, this is especially dangerous because the same environment may host multiple tools, prompts, and logs that can leak or misuse the key.
The MCP configuration example embeds a private key directly in the LLM-connected client configuration. That gives a language-model-adjacent toolchain direct access to a spend-authorizing credential and materially raises the risk of prompt-induced misuse, local compromise, or accidental disclosure.
The skill explicitly states it loads secrets from a local .env file into the runtime for transaction signing. Even though this is presented as a security feature, any agent-integrated tool that reads private keys from environment-backed local files materially increases risk: compromise of the runtime, prompt-induced transaction execution, verbose logging, misconfigured containers, or directory exposure could lead to irreversible theft of on-chain assets.
graph TD
A[AI Agent / LLM Interface] -->|1. Generates CLI Command| B(web3-ops Runtime)
subgraph Local Secure Host Sandbox
B -->|2. Loads Secrets| C[.env File / Host Process]
B -->|3. Signs Tx Client-Side| D[Ethers.js Signer]
D -->|4. Private Key Purged| E((RAM Garbage Collection))
end
The skill explicitly loads a .env file from the parent directory, creating direct access to locally stored credentials. In combination with wallet construction later in the module, this grants the skill privileged access to private blockchain keys and other secrets without any visible trust boundary.
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Load .env from root of skill directory
dotenv.config({ path: path.join(__dirname, "../.env") });
// ANSI Terminal Colors
The module initialization path includes secret loading from .env, meaning credential access happens as soon as the file is imported. That pattern is dangerous because benign-looking operations elsewhere in the skill may inadvertently gain access to sensitive data and signing authority.
const __dirname = path.dirname(__filename);
// Load .env from root of skill directory
dotenv.config({ path: path.join(__dirname, "../.env") });
// ANSI Terminal Colors
export const COLORS = {
This monitor is not passive: when a price threshold is met, it immediately initiates a token swap via swapOrBridge. That couples observation with irreversible asset movement, so a transient price spike, manipulated price feed, bad configuration, or misunderstood invocation can directly trigger an unintended trade and loss of funds.
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
// Generate new random wallet and save to .env
export async function createNewWallet(options = {}) {
const envPath = path.join(__dirname, "../.env");
const force = !!options.force;
try {
This code writes a newly generated private key and mnemonic into a plaintext .env file. Even with mode 0600, plaintext secret storage is risky in agent environments because the file may be copied into backups, committed accidentally, read by other local processes under the same user, or exposed through workspace tooling.
newEnvContent = setEnvVar(newEnvContent, "PRIVATE_KEY", pkLine);
newEnvContent = setEnvVar(newEnvContent, "MNEMONIC", mnemonicLine);
// Write to .env
fs.writeFileSync(envPath, newEnvContent, { mode: 0o600 });
if (!options.json) {
The function generates a new wallet, writes the private key and mnemonic to a local .env file, and then returns and prints those secrets in structured output. In JSON mode, the warning banner is skipped but the secrets are still emitted, which makes accidental leakage through logs, agent transcripts, CI output, or downstream tool capture very likely.
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
const basename = path.basename(src);
if (
basename === 'package-lock.json' ||
basename === '.env' ||
basename === '.gitattributes'
) {
return;
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
const basename = path.basename(src);
if (
basename === 'package-lock.json' ||
basename === '.env' ||
basename === '.gitattributes'
) {
return;
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
copyRecursiveSync(packageRoot, dest);
console.log(`${colors.green}✔ Files copied successfully.${colors.reset}`);
// Create .env from .env.example if not exists
const envExamplePath = path.join(dest, '.env.example');
const envPath = path.join(dest, '.env');
if (fs.existsSync(envExamplePath)) {
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
copyRecursiveSync(packageRoot, dest);
console.log(`${colors.green}✔ Files copied successfully.${colors.reset}`);
// Create .env from .env.example if not exists
const envExamplePath = path.join(dest, '.env.example');
const envPath = path.join(dest, '.env');
if (fs.existsSync(envExamplePath)) {
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
copyRecursiveSync(packageRoot, dest);
console.log(`${colors.green}✔ Files copied successfully.${colors.reset}`);
// Create .env from .env.example if not exists
const envExamplePath = path.join(dest, '.env.example');
const envPath = path.join(dest, '.env');
if (fs.existsSync(envExamplePath)) {
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
copyRecursiveSync(packageRoot, dest);
console.log(`${colors.green}✔ Files copied successfully.${colors.reset}`);
// Create .env from .env.example if not exists
const envExamplePath = path.join(dest, '.env.example');
const envPath = path.join(dest, '.env');
if (fs.existsSync(envExamplePath)) {
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
copyRecursiveSync(packageRoot, dest);
console.log(`${colors.green}✔ Files copied successfully.${colors.reset}`);
// Create .env from .env.example if not exists
const envExamplePath = path.join(dest, '.env.example');
const envPath = path.join(dest, '.env');
if (fs.existsSync(envExamplePath)) {
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
copyRecursiveSync(packageRoot, dest);
console.log(`${colors.green}✔ Files copied successfully.${colors.reset}`);
// Create .env from .env.example if not exists
const envExamplePath = path.join(dest, '.env.example');
const envPath = path.join(dest, '.env');
if (fs.existsSync(envExamplePath)) {
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
copyRecursiveSync(packageRoot, dest);
console.log(`${colors.green}✔ Files copied successfully.${colors.reset}`);
// Create .env from .env.example if not exists
const envExamplePath = path.join(dest, '.env.example');
const envPath = path.join(dest, '.env');
if (fs.existsSync(envExamplePath)) {
This command explicitly manages wallet credentials by generating a new EVM wallet and configuring its secrets in a .env file, which is plaintext credential handling. Private keys and mnemonics stored this way are highly sensitive and may be exposed via filesystem access, shell history, backups, CI artifacts, or accidental repository commits, leading to full wallet compromise.
// 14. Create EVM Wallet
program
.command("create-wallet")
.description("Generate a random EVM wallet and configure its credentials in the .env file")
.option("--force", "Force overwrite of existing PRIVATE_KEY or MNEMONIC in .env", false)
.action((options) => {
createNewWallet(getMergedOpts(options));
The --force option allows overwriting existing PRIVATE_KEY or MNEMONIC values in the .env file, amplifying the credential-handling risk by enabling destruction or replacement of active wallet secrets. In an agent environment, misuse of this option could sever access to funds or redirect future transactions to a newly generated wallet under unintended control.
program
.command("create-wallet")
.description("Generate a random EVM wallet and configure its credentials in the .env file")
.option("--force", "Force overwrite of existing PRIVATE_KEY or MNEMONIC in .env", false)
.action((options) => {
createNewWallet(getMergedOpts(options));
});
This server exposes high-risk wallet operations such as transfer, swap, bridge, mint_nft, and custom_tx directly to an MCP client with no built-in confirmation, policy gate, address allowlist, simulation enforcement, or human approval workflow. In an agent context, that makes prompt injection, tool misuse, or model error materially more dangerous because a caller can trigger irreversible blockchain transactions and asset loss.