# Eve Agent Security Hardening

A prompt for a coding agent. Paste it into Claude Code, Cursor, Codex, or the coding agent of your choice while it is pointed at an Eve project repository.

This prompt can be used in either **Plan Mode** or **Execution Mode**, depending on the user's request.

It contains nine focused security passes. Work through only the passes the user requests, and do not combine multiple passes unless explicitly instructed.

## How to use this

Open your Eve project.

Paste the Operating Rules section along with the pass you want the coding agent to evaluate or complete.

If you are in **Plan Mode**, inspect the repository and produce a detailed implementation plan for that pass without changing files.

If you are in **Execution Mode**, inspect the repository, make the necessary changes, review the diff, and commit the completed pass.

Passes 1 through 3 are the ones that close real holes fastest. If you only have an hour, start with those.

## Operating rules

You are reviewing or hardening an existing Vercel Eve agent for production. Follow these rules for every pass.

### Follow the requested mode

Determine whether the user wants you to plan the work or execute it.

- In **Plan Mode**, inspect the actual project and produce a precise implementation plan. Identify the files that need to change, the relevant Eve APIs, the tests that should be performed, and any decisions or manual setup required. Do not edit files or create commits.
- In **Execution Mode**, make the required changes directly in the project. Do not produce a strategy document or roadmap instead of completing the work.
- If the requested mode is unclear, ask the user whether they want a plan or implementation before continuing.

### Use the current Eve documentation

Read the real documentation instead of relying on memory. Eve is in beta, and its APIs can change.

The complete documentation ships inside the package at `node_modules/eve/docs`. Read the relevant page before planning or writing code.

If the package is not installed, use `eve.dev/docs`.

Check the installed Eve version in `package.json` and make sure all recommendations or code match that version.

### Keep each pass focused

Handle one pass at a time unless the user explicitly requests multiple passes.

Do not address issues from later passes early. Do not refactor unrelated code or reformat files that do not need to change.

In Execution Mode, each completed pass should result in one focused commit using the commit message provided in that pass.

### Fail closed

If a security control cannot be configured correctly, keep the stricter setting in place and explain what is blocking completion.

Never weaken a security setting simply to make something work.

### Do not invent security APIs

If you cannot find the required Eve API in the documentation for the installed version, say so instead of suggesting or writing a plausible-looking API call.

### Report the result

At the end of each pass, provide three short lines:

- Files that would change or were changed
- What the plan or implementation enforces
- What the user still needs to decide or complete manually

In Plan Mode, describe proposed changes without implying that they have already been implemented.

In Execution Mode, report only what was actually changed and verified.


## Pass 0: Inventory
Produce a single short table, not a document. Keep it under 40 lines. Write it to SECURITY-INVENTORY.md at the repo root.

List every one of these that exists in the project:

Files under agent/tools/ with: tool name, whether it writes or has an external side effect, whether it has an approval policy, whether its output comes from an untrusted source.
Files under agent/channels/ with: channel type, current auth policy, whether signature verification is configured.
Connections (MCP, OpenAPI) with: name, auth scope (app or user), token scope granted.
Subagents under agent/subagents/ with: their tool sets.
Schedules with: what they run and which tools they can reach.
The sandbox config: backend and current networkPolicy.
Every secret read from process.env and which tool reads it.

Then flag every tool where all three of these are true: it can reach untrusted content, it can reach private data, and it can send data outward. Those are the highest risk items. Mark them in the table.

**Commit:** `chore: security inventory`


## Pass 1: Route auth fails closed
Eve fails closed by default. Production traffic is rejected unless an authenticator accepts it, and anonymous access requires an explicit none(). The scaffold ships placeholderAuth(), which returns a structured 401 in production so a half-configured app has a clear error instead of open access. It must be replaced before a real browser caller hits production.

### Do this

Open agent/channels/eve.ts. If it does not exist, create it, because the framework default channel also rejects production traffic but gives you nowhere to put a real policy.
Remove placeholderAuth() and put a real AuthFn in the walk. The shipped helpers are vercelOidc(), httpBasic(), jwtHmac(), jwtEcdsa(), oidc(), and none(). If the app already has users, sessions, or API keys, write a custom AuthFn that maps a verified session into a SessionAuthContext and put it first in the array.
Keep localDev() last. It only authenticates while the process is an eve dev or vercel dev server, and that is a property of the deployment, not the request, so no header can flip it on in production.
If you use vercelOidc() with subjects, build the pattern with vercelSubject(...) rather than hand-writing the subject string. A typo silently rejects everyone and a stray wildcard silently admits unrelated projects.
If the app is multi-tenant or multi-user, add the per-session authorization it needs. Route auth decides who can reach the route. It does not enforce session ownership. Two users who both pass route auth can otherwise reach the same session.
If any caller uses defineRemoteAgent({ forwardPrincipal: true }), add a trustedForwarders predicate that matches an exact forwarder subject. Never () => true.
Wrap any custom AuthFn with withAuthChallenges so a 401 advertises the right scheme.

### Done when

an unauthenticated request to the deployed POST /eve/v1/session returns 401, and an authenticated one succeeds. Test both. Do not skip the negative test.

**Commit:** `feat(security): replace placeholder route auth`


## Pass 2: Channel verification and identity
Every channel is a front door. Verify what comes through it.

### Do this

For each platform channel (Slack, GitHub, Telegram, Twilio, and so on), confirm the signing secret is set in the environment and that the deployment reads it. Missing signing secrets are the most common way an inbound webhook becomes forgeable.
For every custom channel, verify the platform HMAC over the raw request body using a constant-time comparison. Never === on a signature. Timing a === comparison can leak the correct signature byte by byte.
Search every channel and route handler for identity taken from the request body. Any principalId, userId, teamId, or similar read directly out of the body is attacker controlled. Derive identity from the verified signature or token instead. Report every instance you find even if you cannot fix it.
If a custom Slack hook such as onAppMention is defined, check whether it dropped the built-in default that derives workspace-scoped auth. If so, attach real caller identity instead of auth: null.
Consider createIpAllowList(...) from eve/channels/auth for internal or operator-only routes. It drops requests before auth and before any model work runs.

### Done when

every channel verifies its signature, no route derives identity from the body, and you have listed each channel with the mechanism that authenticates it.

**Commit:** `feat(security): verify channel signatures and caller identity`


## Pass 3: Approval gates on side effects
By default an omitted approval behaves like never(), so tool calls execute with no human in the loop. This is the single highest-leverage pass.

### Do this

For every tool in the Pass 0 inventory that writes, sends, spends, deletes, publishes, or has any external side effect, add an approval policy. Import from eve/tools/approval. Use always(), once(), or a predicate.
Prefer a predicate when the risk depends on the input. A refund tool can gate on amount. A query tool can gate on estimated scan size. The policy receives the session context plus { toolName, toolInput, approvedTools, callId }.
Gating also makes non-idempotent work safe across durable replays. A charge or an email behind always() cannot re-fire from a re-run step without a fresh human decision. Check for any non-idempotent tool that is currently ungated and flag it even if you gate it.
If schedules or automated turns should skip the prompt while human callers still get one, write a policy that checks session.auth.current for the app principal (authenticator: "app", principalId: "eve:app", principalType: "runtime"). Match all three fields, not one.
Leave read-only tools ungated. Do not add friction where there is no side effect.

### Done when

every side-effecting tool has an explicit approval policy, and no tool relies on an omitted default.

**Commit:** `feat(security): gate side-effecting tools on approval`


## Pass 4: Injection guards at untrusted boundaries
Injected content reaches an eve agent through two boundaries: user messages arriving via channels, and tool results carrying external content back into the model's context. The second is the one people miss.

Enforce inside the tool handler, not in a hook. Hooks run only after the event is durably recorded. A thrown hook fails the turn, but a hook cannot sanitize or replace a tool result before the model sees it. Use hooks for audit logging only.

### Do this

Install a guard. The path Vercel documents is Arcjet Guards: pnpm i @arcjet/guard, with ARCJET_KEY in the environment. Tools run in the app runtime, so process.env is available inside every handler with no extra wiring.
Create one shared client under agent/lib/, exporting a launchArcjet(...) instance and a detectPromptInjection() rule. Every tool imports the same instance.
In every tool whose output comes from an untrusted source, call the guard on the content before returning it. That means fetch tools, document loaders, tools backed by external APIs, and connection-provided tools. One guarded fetch tool does not protect the others.
On a DENY, return a safe placeholder string rather than throwing. That keeps the turn alive so the agent can tell the user the content was unusable and keep working.
Add the same rule to inbound channel hooks (onAppMention, onDirectMessage, and any custom equivalent). A denied message never starts a session, so no tokens are spent.
Keep denied responses vague. Do not tell the user what was flagged or which rule matched. That detail helps an attacker iterate toward an injection that gets through.
If a separate HTTP app fronts the agent, run the same rule there with the request-based SDK. Use dry-run mode first to measure false positives before enforcing.

### Done when

every untrusted boundary in the inventory has a guard call, and a test page containing override instructions comes back as a placeholder in eve dev.

**Commit:** `feat(security): guard untrusted content at tool and channel boundaries`


## Pass 5: Sandbox egress and secrets
The trust boundary is real but narrow. The app runtime is the trusted side with process.env, your Node code, and connections. The sandbox has no process.env, no secrets, and no path back into the app runtime. But tools run in the app runtime, not the sandbox, so the sandbox does nothing to protect a tool you wrote from a malicious argument.

The sandbox default network policy is allow-all. That is an open exfiltration path.

### Do this

Set an explicit networkPolicy on the sandbox backend factory or in onSession's use(). The forms are "allow-all", "deny-all", or an allow list of domains. Start from deny-all and add back only the domains the agent actually needs. Note that deny-all blocks DNS too.
Where the sandbox needs authenticated egress, use credential brokering rather than passing a token in. On the Vercel Sandbox backend, auth headers are injected at the firewall for matching domains, so the secret stays in the app runtime and the sandbox process only sees the response. Scope the matching domains narrowly.
Be careful with catch-all rules. When a policy mixes a * rule with per-domain transforms, connections with no detectable domain (TLS without SNI, or SSH) pass through unmodified. Use a restrictive allow list with no catch-all if domain-less traffic should be denied.
Grep the repo for secrets in compiled artifacts, seed files, or anything passed into the sandbox. Everything privileged should be reached through a tool or a connection in the app runtime.
For every tool, treat arguments as attacker controlled. A Zod type is a shape check, not a safety check. Validate values: allow-list URLs and hosts, reject path traversal, parameterize queries, bound numeric ranges. Report any tool that interpolates a model-supplied string into a shell command, a SQL string, a file path, or a URL.

### Done when

networkPolicy is explicit and tighter than allow-all, no secret reaches the sandbox, and every tool validates its inputs by value.

**Commit:** `feat(security): restrict sandbox egress and validate tool inputs`


## Pass 6: Connection token scope
Connection tokens reach hosts but never reach the model. That limits one risk and not the other. If the token is broadly scoped, an injected instruction can still drive the tool that holds it.

### Do this

For each connection, reduce the granted scope to the minimum the agent needs. A read-only GitHub token caps the blast radius no matter what the model is talked into.
Where a service supports it, narrow the grant at the provider. For Vercel Connect, tokenParams takes explicit OAuth scopes, resource indicators, and authorization details, so a GitHub connection can be pinned to specific repositories.
Check principalType. Use "user" scope where the external service should act as the signed-in person, and "app" scope where it should act as the agent. App scope on something that should be per-user is a cross-tenant leak.
If user-scoped connections are used, confirm route auth actually returns a user principal with a stable principalId. Otherwise token lookup fails with principal_required, or worse, two identity providers share a grant. Include an issuer when the app accepts users from more than one provider.

### Done when

every connection has a documented scope and a reason it needs that much.

**Commit:** `feat(security): scope connection credentials to least privilege`


## Pass 7: Autonomy surfaces
Schedules and subagents multiply everything above, because nobody is watching.

### Do this

For every schedule, list the tools it can reach. A scheduled run that ingests untrusted content while holding write tools is the worst case in the whole system. Cut its tool set to the minimum, or gate its writes on approval so it parks until a person responds.
For every subagent, guard at the subagent's own tool boundary. A subagent that reads a hostile page and summarizes it up to the parent has laundered the injection into what looks like trusted internal output. The parent cannot tell the difference.
Confirm subagent tool sets are narrower than the parent's, not equal to it.
Check skill and schedule files. Eve treats their YAML frontmatter strictly as data and disables the code-capable ---js engines, so such a fence throws rather than running. But skills are still instructions loaded into the model's context. If the agent can write files into its own repo, or if a pull request can add a skill file, that is a persistence path. Report every way a skill file can be created or edited by something other than a human review.

### Done when

every schedule and subagent has a justified minimum tool set, and you have listed every path by which a skill file can change.

**Commit:** `feat(security): constrain schedules and subagents`


## Pass 8: Output handling
### Do this

Find every place model output or user-controlled text is rendered into a channel UI, a web view, an email, or a Slack block. Escape it for that surface. Unescaped agent output into a rendered surface is an XSS vector.
Check that tool errors do not echo secrets, stack traces, internal hostnames, or environment values back into the model's context. Whatever reaches the context can reach the user.
Confirm telemetry and eval exporters are not shipping sensitive session content to a third party you have not reviewed. Eve sends data where your configuration sends it, including to whatever exporters are set in instrumentation.ts.

**Commit:** `feat(security): escape rendered output and scrub error surfaces`


## Pass 9: Prove it
Do not mark this done on inspection alone.

### Do this

Add eval cases under the project's eval suite for the failure modes above. At minimum: a fetched page containing override instructions, a user message containing a jailbreak, and a request that should trigger an approval gate.
Run eve dev and manually try each one. Confirm the guard returns a placeholder, the gated tool parks instead of executing, and the unauthenticated request returns 401.
Run eve eval against the deployment so a later prompt change or model swap shows what broke before users find it.
Pin the eve version in package.json. Eve is in beta and the framework, APIs, and behavior may change before general availability. Read the changelog before upgrading.
Write the final state into SECURITY-INVENTORY.md, replacing the Pass 0 version. Keep it a table.

**Commit:** `test(security): eval coverage for injection, approval, and auth`


## Source checklist
These passes cover the pre-production checklist from eve's own security model docs (eve.dev/docs/concepts/security-model):

Replace placeholderAuth() with a real AuthFn and verify an unauthenticated production request returns 401.
Verify channel signatures. Each platform channel needs its signing secret. Custom channels verify in constant time and never trust body-supplied identity.
Keep secrets in process.env, never in compiled artifacts, never passed into the sandbox. Route privileged calls through tools or connections.
Scope connection tokens to least privilege.
Set a sandbox network policy tighter than allow-all, and use credential brokering for authenticated egress.
Do not surface untrusted text as markup. Escape model- or user-controlled strings for the surface they render on.

Plus, from eve's responsible use terms: unless you configure stricter controls, eve agents may run with permissive settings, including tool execution without human approval where approval is omitted, and sandbox egress that is not deny-all. Do not rely on model behavior alone to prevent sensitive or irreversible actions.

Passes 4, 7, and 8 go beyond the official checklist. They come from the prompt injection guide and from how the framework's own primitives compose.
