# My AI trading tool would sign a wallet drain as a login challenge

> Source: <https://dev.to/edycutjong/my-ai-trading-tool-would-sign-a-wallet-drain-as-a-login-challenge-7o7>
> Published: 2026-09-25 01:44:23+00:00

BagOS is an MCP server I maintain. It lets an AI agent read token data on Bags, a Solana launchpad,

and, if you configure a wallet, trade and claim creator fees. I built it around one idea: a model

should be able to *propose* a spend but never complete one on its own. The first call to a write

tool signs nothing. It returns a preview and a single-use token bound to the exact arguments. Every

trade is capped. Every transaction is simulated before it's signed.

This week an outside review showed that none of that mattered. There was a way to drain the wallet

without touching a single write tool.

It needed two flaws. Either one alone was harmless.

**1. The server trusted the folder you had open.** MCP clients such as Claude Code start a local

server in the current project folder. BagOS called `dotenv.config()`, which reads `.env` from the

working directory. So any repository you opened could supply configuration you never set, including

`BAGS_API_URL`, the endpoint the login tool talks to.

**2. The login tool signed whatever it was given.** `bags_authenticate` proves you own a wallet: it

fetches a challenge from the auth endpoint, signs it, and trades the signature for an API key. It

signed the challenge bytes without checking what they were.

On Solana, a transaction signature is an ed25519 signature over the transaction's serialized

message. So if the "challenge" is a transaction message, the signature the tool sends back is a

valid signature for that transaction. Whoever receives it can broadcast it.

Put them together. A repo ships a `.env` that points `BAGS_API_URL` at a server its author

controls. You open the repo, and your agent calls `bags_authenticate`, maybe because a README told

it to. The fake endpoint returns a transfer of your balance as the challenge. The tool signs it and

sends the signature to that server.

The login tool wasn't a write tool, so none of the guardrails applied: no token gate, no cap, no

preview, no confirmation. My docs even said "Signing a challenge is not signing a transaction."

Before the fix, that wasn't true.

The suite had 100% line, branch and function coverage, enforced in CI. Every line of the auth tool

was tested. The tests checked that it fetched a challenge, signed it and exchanged it, and it did

all of that correctly.

Coverage measures which lines run. It says nothing about which inputs you assumed were safe. My

tests used a well-behaved endpoint and a config I wrote myself, because I had never asked who else

could write that config or what else could arrive as a challenge. The missing check wasn't

untested; it had never been written.

3.0.0 is a breaking release, because it changes how configuration loads.

The server no longer reads `.env` from the working directory. You name a file explicitly, and the

path must be absolute:

``` js
const explicit = env["BAGS_ENV_FILE"]?.trim();
if (explicit) {
  // A relative path resolves against the working directory, which is the
  // exact thing this function exists not to trust.
  if (!isAbsolute(explicit)) {
    console.error(/* "refusing BAGS_ENV_FILE=...: it must be an absolute path" */);
    return "refused-relative";
  }
```

If a `.env` is sitting in the working directory, the server says it's ignoring it, on stderr.

The login tool now signs only Bags' exact sign-in text, with the nonce from the same init response.

It also refuses anything that decodes as a Solana transaction, and anything that isn't printable

text:

```
export function isTransactionMessage(bytes: Uint8Array): boolean {
  try {
    const message = VersionedMessage.deserialize(bytes);
    return Buffer.from(message.serialize()).equals(Buffer.from(bytes));
  } catch {
    return false;
  }
}
```

The auth endpoint is pinned to https on `bags.fm` unless the operator sets

`BAGS_ALLOW_CUSTOM_API_URL=true`. The model can no longer choose the keypair path either.

I drafted a private GitHub security advisory, fixed the bug on a temporary private fork, and merged

it from the advisory page. Then:

One trap for anyone doing this with release-please: merging from the advisory page squashes the

private fork into one commit titled "Merge commit from fork." That isn't a conventional commit, so

release-please computed a **patch** version for a breaking change. I caught it by checking the

release PR before merging, and fixed it by pushing an empty commit that restated the breaking

change.

Hours later, I had the review check my launch plan against the code, and it turned up a second

gap.

The spend caps checked the amount the agent asked for. But the swap transaction that actually gets

signed is built by the Bags API, and nothing compared the two. The cap bounded the request, not

the signature.

3.0.5 closes that. Before signing, BagOS reads the wallet's SOL balance, asks the simulation for

the balance afterwards, and refuses if the difference is more than the approved amount plus 0.01

SOL for fees and rent. A fee claim approves nothing, so it may cost fees only. If the simulation

doesn't report the balance, the transaction isn't signed. The check is required on every path,

so no transaction can skip it.

These are documented in SECURITY.md rather than hidden:

`--http` serves `/mcp` on `0.0.0.0`. Don't run it with a funded
wallet. The stdio default is unaffected.
Links: [repo](https://github.com/edycutjong/BagOS) ·

[advisory](https://github.com/edycutjong/BagOS/security/advisories/GHSA-g679-3wq7-mh3m) ·

[SECURITY.md](https://github.com/edycutjong/BagOS/blob/main/.github/SECURITY.md) ·

[npm](https://www.npmjs.com/package/bagos-mcp-server)

Earlier I wrote about a different BagOS bug, where the write tools reported success without signing anything: [I shipped an MCP server that reported success without signing anything](https://dev.to/edycutjong/i-shipped-an-mcp-server-that-reported-success-without-signing-anything-6oh).

I build safety layers for AI agents that move money. I'm open to remote work.
