Causly Lab

Causly Lab

Ideas, builds and systems for a more autonomous future.

Get insights in your inbox

High-signal content on AI, automation and building systems.

Have an idea?

Suggest a topic or request a deep dive.

A clearer tomorrow built together

- Causly

← Lab

How I Hardened an AI Agent Tool Server Before Launch

Giving an AI agent real tools on your machine is useful, and risky. Before launching Causly Server 1.6, I went through a full security pass: operator approval and a policy file, a workspace limit that covers Docker mounts, minimal environments for child processes, credential redaction, SSRF and prompt-injection checks, and a read-only mode. Here’s what I changed, why, and what’s still not covered.

November 10, 2026

How I Hardened an AI Agent Tool Server Before Launch

An MCP server is the agent's hands on your machine. It reads files, runs commands, talks to Slack and Gmail, and touches your Docker setup. A bug in a normal app is a bug. A bug in this kind of server can become "the agent did something you never meant it to do."

Before launching Causly Server 1.6, I spent a week on a security pass. I started from an external audit and re-checked every claim against the code before acting on it. Then I worked through the fixes in order of risk.

This post covers what I changed, why, and what is still not covered.


The rule I worked from

Assume the agent can be wrong, and assume the content it reads can be hostile.

An agent can misunderstand you. An email it summarizes can contain instructions. A tool argument can be crafted. So the server should not trust the agent, and the agent should not trust what it reads.


1. Ask before risky actions

Causly Server already classified tools by risk (READ, LOW, MEDIUM, HIGH, DESTRUCTIVE) and blocked the dangerous ones unless the call included confirm: true. The problem is that an agent can simply send confirm: true.

In 1.6, risky operations now ask you, through MCP elicitation, so the approval prompt comes from your client and not from the model. You can also write rules ahead of time in ~/.causly/policy.json, so routine actions don't nag you and dangerous ones stay gated.


2. Keep the agent inside a workspace

A file tool that can read anywhere can read your SSH keys. causly-server init now sets a workspace folder (CAUSLY_WORKSPACE), and file operations stay inside it.

The harder part was everything else that touches the filesystem:

  • Docker run volumes are checked against the workspace.
  • Compose files are resolved with docker compose config, so the limit applies to what Compose actually mounts and not only to what the file looks like.
  • MCP project resources follow the same limit.
  • Docker arguments can't start with -, which stops a path from being read as a flag.

I also closed a Docker Compose injection issue and added a guard to copy_file.


3. Don't hand over every key

Causly keeps your service credentials in ~/.causly/.env. Early on, every command the agent ran inherited all of them. A harmless npm test could see your Slack token.

Child processes now start with a minimal environment. If you really need a variable to pass through, you list it in CAUSLY_PASSTHROUGH_ENV. Setting it to * brings back the old behavior, which I don't recommend.


4. Treat what the agent reads as untrusted

Prompt injection is the one I can't fully fix, because the problem is in how models read text. What I can do is make it harder:

  • Results from Gmail and GitHub reads now carry a notice that the content is untrusted data and not instructions.
  • A test suite (tests/promptInjection.test.js) checks that the notice is there.
  • I closed an SSRF hole in http_check, which could otherwise be pointed at internal addresses.

The notice doesn't guarantee that a model will ignore a malicious email. It gives the model a clear signal and gives the approval step something to catch.


5. Keep secrets and logs safe

Several small fixes in this group:

  • Redaction. Credentials are removed from logs and error messages.
  • Secrets store. It uses AES-256-GCM, requires a strict 64-hex master key, and store_path is limited to your data folder or workspace and to files that are real stores. Reading a secret is a HIGH-risk action.
  • Owner-only files and atomic writes, so a crash can't leave a half-written config and other users on the machine can't read it.
  • Activity log. It rotates at 10 MB and keeps 5 files (CAUSLY_LOG_MAX_BYTES and CAUSLY_LOG_KEEP). Each entry is one escaped JSON line, so a tool argument with a newline can't forge a fake log entry, and entries are capped at 16 KB.
  • Consistent errors. All 12 service integrations now share one error handler, so failures don't leak tokens or raw responses.

6. Limits you can set yourself

Some protections should be your choice:

  • CAUSLY_READ_ONLY lets the agent look but not change anything.
  • CAUSLY_MAX_* variables cap output and file sizes. When something is cut off, the result says so, so the agent knows it didn't see everything.
  • CAUSLY_STRICT_PROJECT_COMMANDS turns on a stricter mode for commands taken from the project itself.

How I tested it

Security fixes that aren't tested are guesses.

  • More than 1,700 automated tests run on every change.
  • CI runs on Ubuntu, macOS and Windows, with Node 22 and 24.
  • File-permission behavior was checked on Linux through WSL.
  • The Docker changes got a real end-to-end check with Docker and WSL.
  • A Windows install failure reported in issue #2 was fixed and verified with a live npm install.

What this does not cover

  • The server runs with your permissions. If you approve an action, it runs as you.
  • Prompt injection is reduced, not solved.
  • The workspace limit only works if it's on. init sets it, but you can remove it.
  • Some ideas are waiting for real users: per-project policies, audit export, and workspace profiles. I'd rather build them when someone needs them than guess.
  • I don't have a Mac to test on by hand. macOS is covered by CI.

If you find a hole I missed, please open an issue. For anything sensitive, contact me privately first.


Try it

npx causly-server init