yaah is a Go command-line tool and runtime that writes the configuration for other coding agents — hooks, skills, sub-agents, slash commands and MCP servers — and then runs alongside them as a hook dispatcher, a local MCP server and a per-session audit store. It is not itself an LLM agent: everything it receives comes either from the developer at the keyboard or from the coding agent it serves, and the second of those is model-generated, which makes the hook payloads and the MCP tool arguments the untrusted side of the boundary. Its two consequential surfaces are the safety controls it enforces on the host agent's actions — a dangerous-command guard and a credential scanner — and the configuration it writes into the developer's repository, which decides what the host agent may do, which servers it launches, and what third-party instruction text it loads every session.
Three things are worth fixing before anything else. A skill fetched from a third-party repository is copied into the coding agent's skill tree with the remote author's own front matter intact, so that author, not the harness, decides which tools the skill may use — including shell access — and the grant is re-read every session; the equivalent path for sub-agents already strips that front matter and is the model to copy. The lint tool exposed over MCP takes a file path and a profile straight from the model and runs external programs built from them, three of which fetch and execute a package from the public npm registry, and the same tool surface will read any absolute path the caller names and create directories anywhere it points; one tool call from a coding agent that has read poisoned content is enough to install and run someone else's code on the developer's machine. And the two controls the harness exists to provide are thinner than they look: the command guard is seven text patterns over a shell string, so the obvious rewrites of everything it names pass through; the credential scanner runs after the write, so a secret is already on disk when the block fires; and generating for one of the four supported coding agents drops both controls entirely without a warning.
Two more should be scheduled rather than ignored. The Notion integration writes the operator's API token in clear text into the config files every generator emits, and nothing keeps those files out of version control. And every generated workflow command is invocable by the model itself — including the one that runs a full plan, execute and verify cycle with no human checkpoint — because the switch that would prevent it exists in the generator and is never set. Underneath both, the harness ships the host agent's permission rules, sandbox mode and server allowlist empty even though it fully supports them, and neither safety control has a single test in a repository that otherwise has fourteen test files.
- Remote skill/agent repositories Untrusted origin: markdown authored in a third-party GitHub repository that yaah ships in its default catalog — nobody on the yaah side reviews its content, and the repository owner can change what a future pin points at.
- Remote source cache The repository is cloned into the local cache and the requested file plus every sibling beside it is read back with no content inspection.
- Remote skill content [PRAX-2026-08-12-009] The remote bytes are stored unchanged — no frontmatter strip, unlike the remote-agent path — so the third-party allowed-tools, model, agent and disable-model-invocation values survive intact.
- Harness runtime [PRAX-2026-08-12-009] The hijack: because the remote body already begins with a fence, the writer emits none of its own frontmatter and passes the third-party block through verbatim.
- Generated host config The file lands in the host agent's skill tree with the remote author's tool grant on line 10 — Bash(bash:*) among them — and is committed alongside the repository.
- Host coding agent Consequence: the coding agent reads that grant every session, so a remote author, not the operator, sets what the skill may execute — a persistent capability change that outlives any single run.
- Host coding agent Untrusted origin: an unauthenticated stdio MCP caller whose arguments are model-generated, produced by an agent whose context routinely holds third-party content — including the remote skills yaah itself installed.
- yaah MCP server [PRAX-2026-08-12-008] The hijack: the file path and profile name are taken as given, with no containment against a project root and no guard consulted, and the working directory is derived from the caller's own path.
- Lint execution engine [PRAX-2026-08-12-006] Consequence: the chosen profile's steps are executed as external processes, and three of them run npx, which downloads and executes a package from the public registry on the developer's machine.
- Host coding agent Untrusted origin: a Bash command proposed by the host agent's model, which is exactly the input the guard exists to distrust and which any content the agent has read can shape.
- yaah CLI The hook payload is read from stdin and parsed with no authentication and no restriction on what a caller may claim.
- Harness runtime The hijack: the dispatcher hands the raw command string to the PreToolUse handlers and will allow the action unless one of them returns a block.
- Command guard [PRAX-2026-08-12-004] The denylist is seven regexes over the flat string, so rm -fr /, git push -f origin main, TRUNCATE, a reversed dd or any of them wrapped in an alias or sh -c matches nothing and is reported safe.
- Harness runtime With no handler blocking, the combined result carries no block flag and the hook process exits zero.
- Host coding agent Consequence: the host agent treats the zero exit as approval and executes the destructive command on the developer's workstation.
B1 Host coding agent into the yaah runtime (untrusted-ingress) — 3 threats, 3 remit rules · worst: confirmed
R-25 partial A file edit that introduces a hardcoded credential MUST NEVER be written — the secret scanner blocks it (fail closed).
R-01 verified yaah MUST NEVER treat the content of fetched remote skills or agents, MCP tool descriptions, or scanned/linted file contents as instructions that change its own behavior or safety decisions
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| E | LLM10 | A destructive command rewritten past the seven-pattern denylist — rm -fr / instead of rm -rf /, git push -f instead of --force, TRUNCATE instead of DROP, or any of them wrapped in a variable or sh -c — is screened as safe and the host agent executes it. | confirmed PRAX-2026-08-12-004 |
| T | LLM02 | A credential-bearing edit has already landed on disk when the PostToolUse scanner reads the file back, and the block result carries a message but no revert, quarantine or restore, so the fail-closed promise holds only in wording. | confirmed PRAX-2026-08-12-005 |
| T | — | A regression in either safety control would ship undetected: the package holding the guard and the scanner has no test file while fourteen exist elsewhere, and CI's only gate is a blanket go test run. | confirmed PRAX-2026-08-12-013 |
B2 MCP tool calls reaching the filesystem and external binaries (tool-invocation) — 3 threats, 4 remit rules · worst: confirmed
R-05 partial Within the target repository and the yaah cache directory
R-17 partial Files within the target repository, read for secret scanning, linting, and config generation.
R-31 partial Session lifecycle events, tool calls (including MCP-server tool invocations served via yaah serve) ... are recorded to .claude/sessions/<id>.json
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| E | ASI05 | yaah_lint runs each profile step through exec.CommandContext with the working directory derived from the caller's own path, and three built-in profiles invoke npx, so one tool call selects which third-party package is downloaded and executed on the developer's machine — with no guard consulted on this route. | confirmed PRAX-2026-08-12-006 |
| I | LLM03 | yaah_scan_secrets opens whatever absolute path the caller names and reports which credential pattern matched at which line — a probe oracle over files outside the project — and yaah_planning_init creates a five-directory tree and writes STATE.md under any project_dir given, with none of the containment the session store already demonstrates. | confirmed PRAX-2026-08-12-008 |
| R | — | No MCP tool handler writes to the session store, so all seven tools execute with no audit record, and the Findings channel the record declares and the CLI prints is populated by nothing. | confirmed PRAX-2026-08-12-010 |
B3 Generation overwriting the target repository's agent configuration (state-commit) — 2 threats, 2 remit rules · worst: potential
R-28 verified Generated configuration MUST NEVER weaken the host coding agent's permission/sandbox posture
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| T | — | A generate run replaces the settings, MCP and hook files of every supported target in place with no diff, backup or confirmation, so a hand-narrowed posture in the target repository is silently overwritten by the built-in defaults (checked: cmd/yaah/main.go:113 and 127 — unconditional os.WriteFile, no existence check or prompt anywhere on the path). | potential |
| E | LLM03 | Generated configuration places the host coding agent into a permission-bypassing or sandbox-disabled mode by default. | mitigated pkg/harness/defaults.go:294 |
B4 Planning state written by a tool and read back into the model's decisions (stored-state) — 1 threats, 0 remit rules · worst: potential
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| T | LLM01 | STATE.md is created wherever the caller's project_dir points and is later parsed straight back into the model's context as the project's current phase and status, so anything able to edit that file steers later workflow decisions across sessions (checked: pkg/mcpserver/tools.go:302-322 — the frontmatter is split on --- and matched by prefix with no schema, size, ownership or provenance check, and no root containment on the directory it is read from). | potential |
B5 Session audit record readable by the host agent (data-at-rest) — 2 threats, 2 remit rules · worst: confirmed
R-27 verified Session files MUST NEVER be read or written at paths derived from unvalidated session identifiers
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| I | LLM02 | Session records keep the first 120 characters of each Bash command verbatim and the scanner never inspects Bash input, so an inline export or curl header carrying a token persists unmasked in the session file — which yaah_session_info then marshals back to the calling model in full. | confirmed PRAX-2026-08-12-012 |
| T | — | A caller-supplied session id escapes the session directory through path traversal on either the CLI or the yaah_session_info route. | mitigated pkg/session/store.go:153 |
B6 Operator credential embedded in generated configuration (secret-material) — 1 threats, 4 remit rules · worst: confirmed
R-21 gap Credentials, API tokens, and OAuth secrets MUST be sourced from environment variables or a secrets manager.
R-20 partial Credentials, API tokens, and OAuth secrets MUST NEVER be committed to the repository.
R-15 partial Any MCP provider that carries credentials or OAuth (e.g. Notion, remote OAuth servers).
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| I | LLM02 | The Notion provider splices the operator's token into an environment value as a literal Bearer header and every generator copies that value into its output, so the credential lands in five repository config files that .gitignore does not exclude — and the provider is registered on the mere presence of a token, with no approval step for a credential-carrying integration. | confirmed PRAX-2026-08-12-001 |
B7 Third-party skills, agents and MCP servers entering the host (supply-chain) — 3 threats, 4 remit rules · worst: confirmed
R-12 verified Fetching default or shipped remote skills/agents from mutable refs (branches) rather than immutable refs (a commit SHA or version tag).
R-24 partial Enabling any feature that makes outbound network calls or spawns an autonomous subagent ... MUST be off by default and enabled only by explicit operator opt-in.
R-11 partial github.com (and specifically the pinned source repositories above) for remote skill/agent fetch.
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| T | LLM04 | The built-in defaults enable Context7 and the Pulumi endpoint, so a plain generate emits a config whose Context7 entry runs npx with no version specifier — resolving and installing whatever the registry currently publishes on every host start — against a registry host that appears in no trusted-domain list and with no operator opt-in. | confirmed PRAX-2026-08-12-007 |
| T | LLM01 | Remote skill markdown is written into the host agent's skill tree with the third-party author's own frontmatter intact — allowed-tools, model, agent and disable-model-invocation all come from the remote file — and every sibling file beside SKILL.md is copied with no content check or provenance label, while the remote-agent path strips exactly this. | confirmed PRAX-2026-08-12-009 |
| T | LLM04 | Nothing verifies fetched content against the pin after checkout, and the ref parser accepts a branch or tag name in the same slot as a commit SHA, so pinning holds only for as long as every source keeps using one. | partial pkg/harness/defaults.go:492remainder: a Go-library caller may pass any uses string, and gitcache.ParseUses (gitcache.go:16-27) forwards a branch name to the tier-1 clone unchanged |
B8 The host agent's safety surface as yaah ships it (control-plane-exposure) — 3 threats, 3 remit rules · worst: confirmed
R-03 partial yaah MUST NEVER run with its command-guard or secret-scanner safety hooks removed, disabled, or downgraded to a non-blocking / advisory mode
R-28 verified it may not place the host into a permission-bypassing (bypassPermissions / dontAsk) or sandbox-disabled mode by default, nor drop the host below its stated minimum permission/sandbox posture
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| E | LLM03 | Every generated workflow command ships model-invocable because the frontmatter flag that would stop it is emitted by the generator and set by no command, including the autonomous runner whose own body executes discuss, plan, execute and verify with no human checkpoint. | confirmed PRAX-2026-08-12-002 |
| T | LLM03 | Generating for the Codex target emits a working configuration in which neither the command guard nor the secret scanner is wired to any event, because the hook map has no Codex name for the two events they register for and the generator drops unmapped events without a warning or a degraded-mode notice. | confirmed PRAX-2026-08-12-003 |
| E | LLM03 | The emitted settings carry no permission rules, no sandbox mode and no MCP allowlist even though the schema declares all of them and the generator copies all of them, so the harness whose job is configuring the host's safety surface ships that surface empty. | confirmed PRAX-2026-08-12-011 |
| Component | Kind | Lane | Description | Evidence |
|---|---|---|---|---|
| Developer / operator | entrypoint | user_inputs | The human who runs the yaah CLI to generate coding-agent configuration and manage session state; the trusted owner of record. | cmd/yaah/main.go:53; cmd/yaah/main.go:82 |
| Host coding agent | entrypoint | user_inputs | The coding agent yaah runs alongside (Claude Code, OpenCode, Codex, Copilot); it pipes hook event JSON into `yaah hook` on stdin and calls the yaah MCP tools over stdio, so every argument yaah receives from it is model-generated. | cmd/yaah/main.go:178; pkg/hooks/input.go:10 |
| yaah CLI | client | client_adapters | The cobra command surface — generate, hook, serve, session, doctor, skills — and the process that converts a blocking hook result into exit status 2. | cmd/yaah/main.go:59; cmd/yaah/main.go:184 |
| Per-target config generators | adapter | client_adapters | The four generators (Claude, OpenCode, Codex, Copilot) that serialize the harness config into each agent's native format; all four copy MCPServer.Env through unchanged. | pkg/generator/settings.go:220; pkg/generator/codex.go:48 |
| Harness runtime | orchestrator | agent_core | Wires the seven registries, dispatches each hook event through the enlisted handlers, combines their results into a single block decision, records the call in the session, and writes skills, agents and commands into the host tree. | pkg/harness/harness.go:96; pkg/harness/harness.go:121 |
| Command guard | control | agent_core | PreToolUse handler that blocks Bash commands matching seven case-insensitive regexes over the raw command string; anything not matched is allowed. | pkg/hooks/handlers/guard.go:29; pkg/hooks/handlers/guard.go:86 |
| Secret scanner | control | agent_core | PostToolUse handler that reads the edited file back from disk and blocks when one of thirteen credential patterns matches; it reports location and pattern only, never the matched value. | pkg/hooks/handlers/secretscan.go:68; pkg/hooks/handlers/secretscan.go:96 |
| Cross-agent hook map | control | agent_core | Table mapping each Claude hook event to the equivalent event name per target agent; it carries no Codex name for PreToolUse or PostToolUse, and the generator silently drops any event whose mapped name is empty. | pkg/generator/hookmap.go:30; pkg/generator/hookmap.go:35 |
| Host hardening defaults (empty) | control | agent_core | Stub control: the default settings block populates only model, thinking, effort level and update channel, leaving the permission, sandbox and MCP-allowlist fields the schema declares and the generator copies entirely unset. | pkg/harness/defaults.go:294; pkg/schema/settings.go:40 |
| /yaah:* workflow commands | prompt | agent_core | The generated workflow command markdown, including the autonomous phase runner whose body executes discuss, plan, execute and verify in sequence without pausing; no built-in command sets DisableModelInvocation. | pkg/commands/builtins/autonomous.go:18; pkg/commands/builtins/autonomous.go:29 |
| Remote skill content | prompt | agent_core | Loader for third-party SKILL.md files: the fetched bytes are stored unchanged, every sibling file beside SKILL.md is copied along with them, and the writer skips its own frontmatter generation whenever the remote body already starts with a fence. | pkg/skills/remote.go:95; pkg/skills/remote.go:100 |
| Remote agent frontmatter strip | control | agent_core | Control on the remote-agent path: fetched agent markdown has its YAML frontmatter removed before generation, so a third-party repository cannot declare tools, a model or a permission mode for a generated sub-agent. | pkg/agents/remote.go:104; pkg/agents/remote.go:111 |
| Session audit trail | datastore | agent_core | Per-session JSON record of tool calls, blocked calls, modified files and a Findings array, written atomically under .claude/sessions/, alongside the plaintext lifecycle log the session-logger appends. | pkg/session/store.go:52; pkg/session/session.go:6 |
| Session id sanitizer | control | agent_core | Rejects empty identifiers, the special values . and .., and any identifier containing a path separator or a non-base name, before it is used to build a session file path. | pkg/session/store.go:153; pkg/session/store.go:168 |
| yaah MCP server | mcp_server | tools_mcp | stdio MCP server exposing exactly the seven declared tools; caller-supplied file paths, project directories, commands and session ids are used as given, with no project-root containment. | pkg/mcpserver/server.go:43; pkg/mcpserver/tools.go:66 |
| Lint execution engine | tool | tools_mcp | Runs each profile step through exec.CommandContext — three built-in profiles shell out to npx — driven either by the PostToolUse hook or by the yaah_lint MCP tool on a caller-supplied path, with no guard consulted on either route. | pkg/hooks/handlers/linter.go:228; pkg/hooks/handlers/linter.go:79 |
| Remote source cache | datastore | tools_mcp | Clones pinned remote repositories under the yaah home directory using a three-tier git strategy and serves the requested file plus every sibling beside it from the cached checkout. | pkg/gitcache/gitcache.go:52; pkg/gitcache/gitcache.go:107 |
| .planning/STATE.md | memory | tools_mcp | Planning state file created by yaah_planning_init and parsed back by yaah_planning_status, whose stored phase, status and timestamp are returned to the calling model as the project's current position. | pkg/mcpserver/tools.go:410; pkg/mcpserver/tools.go:302 |
| Default outbound MCP servers | external_service | external_deploy | Context7, launched as `npx -y @context7/mcp` with no version specifier, and Pulumi's hosted HTTP endpoint — both enabled by AllDefaults and both present in the committed project config. | pkg/mcp/providers/context7.go:22; pkg/mcp/providers/pulumi.go:21 |
| Notion provider credential | secret_store | external_deploy | MCP provider that holds the operator's Notion API token as a struct field and splices it into an OPENAPI_MCP_HEADERS environment value as a literal Bearer header; no environment-variable indirection or secrets-manager reference exists on this path. | pkg/mcp/providers/notion.go:12; pkg/mcp/providers/notion.go:27 |
| Generated host config | deploy_surface | external_deploy | What yaah writes into the target repository — settings.json, .mcp.json, .codex/config.toml, opencode.json, .copilot/mcp-config.json — plus the generated skill and sub-agent trees the host agent reads every session. | .claude/settings.json:7; .mcp.json:2 |
| Remote skill/agent repositories | external_service | external_deploy | The third-party GitHub repositories yaah ships in its catalog (pulumi/agent-skills, dirien/claude-skills, jeffallan/claude-skills, rshade/agent-skills, msitarzewski/agency-agents and others) whose markdown becomes the host agent's skills and sub-agents. | pkg/harness/defaults.go:492; pkg/gitcache/gitcache.go:26 |
| CI and release pipeline | deploy_surface | external_deploy | The GitHub Actions build and release workflow whose only pre-release check is `go test ./...`, with cosign signing and a syft SBOM at tag time and every action SHA-pinned. | .github/workflows/ci.yaml:35; .github/workflows/ci.yaml:18 |
omissions: No arbitration divergence: on every finding the stored primary tag (first code in the tags array) matches the KB's primary under the same evidence — LLM02 for the embedded credential and for the unredacted command capture (disclosure at rest, ASI03 not applying as a scope defect); LLM03 for the ungated model-invocable commands, the Codex control drop, the empty hardening block and the uncontained tool paths, with ASI10/ASI05/ASI02 riding as co-tags per the outlive and mechanism rules; LLM10 for the weak command guard, since yaah supplies only the inadequate screen and the exec sink is the host agent's, while ASI05 stays primary for yaah_lint where yaah itself execs; LLM04 for the unpinned npx MCP defaults (install-time provenance) and LLM01 for the unlabeled third-party skill ingest, which has no goal-alteration evidence and so does not flip to ASI04; observability and testing gaps carry no OWASP code. Not modeled as nodes: the experimental fact-check hooks (defaults.go:321 — a Sonnet subagent with WebFetch handed to the host runtime as a hook rule, installed only when an environment variable is set, and reachable by no path here); the LSP providers and the Codex marketplace plugin, which fold into the generated-config node's evidence; the comment-checker handler, which carries no distinct trust consequence. The CI/release node is edgeless repository-posture evidence. A hygiene sweep for committed credentials across the tree found no live secret — the single private-key marker is placeholder documentation text inside a fetched third-party skill's references directory, which the scan scope excludes.
generated by: Opus 5 (1M context)