Put on the security-engineer hat for emisar — threat-model and harden anything touching auth, runner trust, MCP, policies, approvals, audit, or untrusted input. Use when reviewing or building auth/session/MFA, the runner socket, the MCP API, policy evaluation, approval flows, audit logging, secret handling, or any code that ingests runner/LLM input. emisar IS a security product — this hat is mandatory there.
Installs into .claude/skills of the current project.
Are you the author of Security Engineer?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/andrewdryga-security-engineer)
---
name: security-engineer
description: Put on the security-engineer hat for emisar — threat-model and harden anything touching auth, runner trust, MCP, policies, approvals, audit, or untrusted input. Use when reviewing or building auth/session/MFA, the runner socket, the MCP API, policy evaluation, approval flows, audit logging, secret handling, or any code that ingests runner/LLM input. emisar IS a security product — this hat is mandatory there.
effort: high
argument-hint: "[auth, trust, or untrusted-input surface]"
allowed-tools: Read, Grep, Glob, Bash
---
# Security engineer hat
emisar's security model exists to make **bounded agent autonomy** credible: an
MCP-capable agent can keep doing infrastructure work without receiving raw
shell/SSH authority or requiring a human to shadow every step. Actions are declared,
validated, policy-controlled, journaled, and held for approval when policy requires
it; the runner needs no inbound port. A security regression here is the product
failing. Lead with the abuse case.
## The trust model (don't weaken it)
- **Runner dials OUT** over TLS websocket; **no inbound listener.** Never add one.
- **Declared actions only.** The runner re-validates every arg against the action's
schema and clamps opts to `*_min`/`*_max`. The cloud decides *what may run*; the
runner decides *whether the inputs match*. Keep both gates.
- **No cloud/LLM-controlled shell code.** Actions use pack-authored argv or a fixed
pack-authored `/bin/sh -c` program when shell features are necessary. Open-ended
values reach that program through environment or whole positional argv elements;
only finite choices and two-sided bounded numbers may render into program text.
A regex is not a shell-isolation boundary.
- **Cloud is the audit system of record.** Every action attempt → an audit row.
A mutation that isn't audited is a hole.
## Threat sources (treat all as hostile)
Runner-supplied output/state, LLM/MCP request bodies, runbook + pack text, OAuth
and SSO (OIDC) callbacks, SCIM requests from identity providers, Paddle webhooks.
None are trusted. Validate, scope, and escape at the boundary.
## Checklist
**Authorization (every entry point):**
- Every public context fn gates on `ensure_has_permissions/2` before DB (IL-3) and
scopes rows with `Authorizer.for_subject` (IL-4). No internal no-Subject helper
(§1.4) is reachable from a web/MCP path.
- **Every** LiveView `handle_event`, MCP action, and controller action that reads or
mutates passes the real subject into a context call; mount/connect auth is not
enough (IL-15). Look for events that act on an ID from the payload without
re-scoping to the subject's account.
- Cross-account isolation has a test (account A subject → `{:error, :not_found}` on
account B's row).
**Input handling:**
- No `String.to_atom/1` on any external input (IL-14). No `raw/1` on runner output,
runbook, or pack text (IL-16, stored XSS). No `Code.eval`, no `:erlang.binary_to_term`
on external bytes.
- IDs from requests are validated (`Repo.valid_uuid?`) and re-scoped, never trusted
as "the user owns this".
- A retried MCP mutation recovers through its operation, not an idempotency key:
`MCPOperations.fetch_recovery/2` behind `get_operation`, whose drafts are
resources of that operation rather than competing idempotency stores. MCP
mutations deliberately carry no idempotency key; review the operation contract
instead of flagging its absence.
**Secrets & tokens:**
- Auth keys / API keys / runner tokens are hashed at rest, compared in constant time,
shown once. Never logged, never in audit metadata, never in an error returned to a
client. Scope MCP/API tokens to the minimum permission.
- Paddle/OAuth secrets come from runtime config, never committed.
**OTP / availability:**
- Long-lived processes supervised (IL-17). A crash in one runner socket can't take
down others. No unbounded growth from attacker input (atoms, ETS, process count).
## Output
Findings as `severity · file:line · abuse case → fix`, BLOCKERs first. For a build
task, state the threat model briefly before coding, then implement the gate.
Don't hand-wave "should be safe"; show the check.