dyad Security & Engineering Audit
You shipped it.
Is it safe to operate?
We audited dyad across 9 engineering areas - 54 findings, 9 of them critical or high. Here's the picture.
https://github.com/dyad-sh/dyad
9 areas evaluated
Each area scored 0-10 by an AnchorStack engineer.
Code Quality
5.7/10
The codebase has a solid foundation: TypeScript strict mode, Zod-validated IPC contracts covering the vast majority of handlers, a well-decomposed utility layer, and comprehensive test coverage at both unit and e2e levels. The critical risk is concentrated in a small number of massively oversized handler files—particularly the 2,196-line chat_stream_handlers.ts whose core streaming path is a single 1,600+ line ipcMain.handle callback that mixes every concern in the application and bypasses the typed contract system entirely. Changes to these files are expensive and risky; the lack of Zod validation on the most critical endpoint is the most actionable structural gap.
Architecture
6.1/10
Dyad is a well-structured Electron modular-monolith: the main/renderer process split is cleanly enforced, IPC is typed and Zod-validated end-to-end, and the local SQLite data model is coherent. The principal architectural risks are concentrated in two areas: (1) all long-running work — LLM streaming, multi-step agent tool loops, git operations, and shell execution — runs directly in the main-process Node.js event loop with no queuing, back-pressure, or worker isolation; and (2) agent file writes and git commits are non-atomic, so a crash or git failure between the two steps leaves disk state and version history diverged with no auto-recovery path. At current single-user desktop scale these are tolerable, but both will become more painful as agent runs grow longer and more users run concurrent sessions or integrate cloud sandbox flows.
Data
6.1/10
Dyad's data layer is a well-structured local SQLite database (better-sqlite3 + Drizzle ORM) with a clean schema, proper FK constraints, WAL mode, cascade deletes, and 30 versioned migrations deployed automatically at startup — a solid foundation for a single-user desktop app. The primary risks are concentrated in two areas: (1) MCP server environment variables (which routinely contain third-party API keys) are stored as plaintext JSON in an unencrypted SQLite file despite awareness of their sensitivity; and (2) backups are only triggered on version upgrades, meaning an entire user session is unprotected between releases. Secondary gaps include a non-transactional compaction write pair, a plaintext fallback in the encryption path with no user warning, and absent down-migration scripts. None of these constitute immediate data-loss risks at current single-user desktop scale, but the MCP secrets gap is a meaningful credential-exposure surface and the backup gap becomes serious if the database is corrupted mid-session.
Security
4.7/10
Dyad has well-engineered Electron process isolation — contextIsolation, a strict IPC channel whitelist, disabled node integration, and a strong fuses configuration (RunAsNode off, ASAR integrity, cookie encryption). File-system access is protected end-to-end with a safeJoin() path-traversal guard. The critical gaps are concentrated in four areas: (1) the OAuth deep-link handler accepts credential-overwrite requests from any local process without a state or PKCE parameter, enabling a local CSRF attack against stored tokens; (2) MCP stdio server commands are not validated against a binary whitelist, so a crafted dyad://add-mcp-server deep link can cause Dyad to execute an arbitrary binary as a child process; (3) the CI pipeline explicitly disables npm audit and has no SAST, SCA, or secret-scanning tooling, leaving known dependency vulnerabilities invisible; and (4) MCP server credentials (API keys and auth headers) are stored in plaintext SQLite despite first-party awareness of the sensitivity. The AI agent has meaningful consent controls but lacks input/output filtering, rate limits, and hardened defenses against indirect prompt injection via fetched web content.
AI Engineering
7.9/10
Dyad operates at an exceptional AI engineering maturity level for an open-source desktop application. The context layer is specific, maintained, and actively compounding: AGENTS.md indexes 17 domain-specific rule files, hard-won learnings are automatically captured after every push via the `remember-learnings` skill, and 20+ purpose-built Claude Code skills automate the entire workflow from multi-agent spec planning (`swarm-to-plan`) through validation and PR delivery. The two main risk surfaces are a policy-code contradiction that auto-approves `gh pr merge` contrary to the stated permission policy, and path-unrestricted `Edit`/`Write` permissions in `settings.json` that bypass the otherwise carefully-layered Bash allow-list. Delivery documentation is the weakest domain: the `pr-push` PR template is minimal and does not require coverage of security implications, data-model changes, or rollback notes, leaving AI-generated PRs with thin audit trails.
Infrastructure
6/10
Dyad is a local-first Electron desktop app distributed as signed binaries via GitHub Releases — no traditional server infrastructure exists for the core product, and the chosen platform is an excellent fit for the use case. The release pipeline is mature: multi-platform CI on SHA-pinned actions, a dry-run/publish separation model, notarized macOS builds, Azure Trusted Signing on Windows, and a post-publish asset verification step all provide a high-confidence distribution path. The principal infrastructure gaps are: (1) the auto-update server at api.dyad.sh is a critical external dependency with no IaC, recovery documentation, or monitoring configuration anywhere in the repo — its hosting, ownership, and rebuild path are completely opaque; (2) release verification confirms file presence but not that a built installer actually launches; and (3) local backup recovery is version-upgrade-only with no in-app restore path and a 3-backup retention cap. The binary distribution infrastructure can be fully rebuilt from the repo; the update-serving backend cannot.
Observability
3/10
Dyad's observability posture is near-zero for its only server-side component (api.dyad.sh, the auto-update backend) and minimal for the desktop client. The team can detect user-reported crashes via PostHog exception capture and electron-log file logging, but both mechanisms have significant gaps: PostHog is opt-in with 90% sampling for non-Pro users, logs are unstructured strings with no correlation IDs or remote retention, and no alerts are configured anywhere. There are no dashboards, no runbooks, no reliability targets, and no monitoring for the update service that all auto-updating users depend on. The system cannot detect production failures, provider API degradation, or abnormal crash rates without a user filing a GitHub issue.
Delivery
5.3/10
Dyad's delivery pipeline is mature for a local-first Electron desktop app: GitHub Actions handles multi-platform builds with sharded Playwright E2E on macOS and Windows, a clean dry-run/publish/verify-assets release model with code-signed binaries, and AI-assisted PR review (Claude + Codex) for trusted contributors. The primary gaps are: npm audit is disabled in CI with no SCA alternative, so dependency vulnerabilities are invisible before release; the LLM agent eval suite exists but is not wired into CI, meaning prompt and agent-behavior regressions ship undetected; no feature flag or kill-switch mechanism means a bad auto-update propagates to all users until a hotfix is published; and an unpinned @openai/codex@latest install in a pull_request_target workflow with secret access creates a supply-chain risk. No staging environment exists and branch protection configuration is not codified in the repository, leaving enforcement of required status checks unverifiable.
Documentation
5.5/10
Dyad's documentation posture is sharply bifurcated: AI-agent context (AGENTS.md, 17 domain-specific rules files, auto-updating skills via remember-learnings) is exceptional and ranks among the strongest seen in an open-source Electron project, while human-facing operational, architectural-decision, and API reference documentation has significant gaps. A new contributor can install dependencies, run the app, and write code from CONTRIBUTING.md alone, but cannot respond to a production incident, renew code-signing certificates, restore from a corrupted database, understand which agent mode (XML-tag vs. tool-calling) applies to a given task, or find the rationale for any key architectural decision in the currently deployed system. The three ADRs in the repository describe unimplemented future cloud features, not the system that ships today, and the update service (api.dyad.sh) that all auto-updating users depend on has no runbook or recovery documentation anywhere in the repo.
Where to look first
01
— Code Quality
Core streaming handler is a 1,600-line god function
The `chat:stream` ipcMain.handle callback in chat_stream_handlers.ts spans roughly 1,600 lines and performs prompt construction, LLM streaming, DB persistence, git operations, file I/O, MCP routing, and cancellation management without any internal decomposition.
Any change to chat behavior—prompt logic, streaming, cancellation, file writes, git commits—requires modifying and reasoning about the entire 1,600-line function. Bugs introduced in one concern (e.g., cancellation) can silently affect another (e.g., DB state). Code review is effectively impossible at this scale.
Extract the major phases into named async functions with clear inputs/outputs: (1) message loading and redo, (2) context/prompt construction, (3) LLM stream loop, (4) response processing/file writes, (5) git commit. Each function should be independently testable. The overall handler then becomes a thin coordinator.
02
— Code Quality
Critical `chat:stream` endpoint bypasses typed IPC contract system
While all other IPC handlers use `createTypedHandler` for Zod-validated input, `chat:stream`—the most frequently invoked endpoint—is registered with raw `ipcMain.handle`, accepting an unvalidated `ChatStreamParams` cast.
Malformed or unexpected IPC payloads from the renderer (e.g., missing chatId, injected fields) reach handler logic without validation, potentially causing DB errors, unhandled exceptions, or unexpected behavior that is harder to diagnose and test.
Define a `defineStream` contract (using the existing `StreamContract` infrastructure in core.ts) for `chat:stream` and migrate the handler to use `createTypedHandler` or a typed stream registration helper so Zod validates input before the handler executes.
03
— Architecture
Agent file writes and git commits are non-atomic
The local agent handler writes files to disk and then issues a separate git commit; a crash, a git authentication failure, or a forced quit between these two steps leaves the working tree modified but unversioned with no auto-recovery.
User loses generated edits without notification. On next launch the app appears clean (no pending changes indicated) but disk state is ahead of git history. Repeated runs can accumulate silent divergence across multiple files.
Stage all file writes to a staging area or in-memory buffer and apply them in a single git operation; or persist a write-intent log to SQLite before touching the filesystem so that an incomplete run can be detected and surfaced (or rolled back) on next launch. At minimum, detect divergence between HEAD and working tree on app open and prompt the user.
04
— Data
MCP server credentials stored as plaintext in unencrypted SQLite
The mcpServers table's envJson and headersJson columns store MCP server API keys and auth headers as plain JSON in the SQLite database file, which is not encrypted at rest; debug handlers explicitly exclude these fields with a comment noting they 'may contain secrets', confirming awareness of the sensitivity.
Any process, backup tool, or forensic artifact that reads ~/.dyad/sqlite.db recovers all MCP server credentials in cleartext. A third-party app with file-system access (common on macOS/Windows) or a compromised backup can silently exfiltrate every API key the user has configured for MCP integrations.
Encrypt envJson and headersJson values using the existing encrypt()/decrypt() helpers from src/main/settings.ts before writing to SQLite — store the Secret envelope (value + encryptionType) as JSON rather than the raw key-value map. Alternatively, migrate MCP credential storage to the encrypted settings file alongside other provider API keys and store only a non-secret reference ID in the database row.
05
— Security
OAuth deep-link handler accepts token writes from any local process
The dyad://supabase-oauth-return, dyad://neon-oauth-return, and dyad://dyad-pro-return handlers extract tokens directly from URL parameters and write them to encrypted settings with no state parameter, nonce, or PKCE verifier check.
Any application running on the same machine — including a malicious website opened in the default browser — can register the dyad:// URL scheme on macOS/Windows and craft a deep link that overwrites the user's Supabase, Neon, or Dyad Pro credentials with attacker-controlled tokens. This gives the attacker control of the user's cloud database and billing account.
Generate a cryptographically random state parameter (e.g., 32 bytes of crypto.randomBytes) before initiating each OAuth flow and persist it to an in-memory store keyed by provider. On deep-link return, verify the state parameter matches before committing tokens to settings. For Dyad Pro API key returns, add a signed nonce to the redirect URL and verify it server-side. Reject and log any return whose state is missing or mismatched.
06
— Security
MCP stdio server command is not validated against a binary whitelist
AddMcpServerConfigSchema accepts any string for the stdio transport command field with no path restrictions, and the MCP manager passes it directly to StdioClientTransport as a child-process command.
A crafted dyad://add-mcp-server deep link (achievable from any local app or a browser navigation) can register an MCP server whose command points to an attacker-controlled binary. If the user clicks through the confirmation UI, the binary is executed with the Electron main process's user privileges each time an AI tool calls the MCP server. This is a reliable local privilege-to-arbitrary-execution escalation.
Restrict the stdio command to a known-safe set of executables: resolve the command via which/where, require it to exist within standard package-manager bin paths (node_modules/.bin, ~/.local/share/..., brew prefix, etc.), and reject absolute paths outside those prefixes. Display the resolved full binary path in the user confirmation dialog. Log all MCP server additions with the full command.
07
— Security
npm audit explicitly disabled in CI; no SCA or SAST tooling present
Every CI install step runs npm ci --no-audit, suppressing the npm security audit. There is no Dependabot, no CodeQL, no Snyk, no SCA gate, and no secret-scanning step in the CI or pre-commit hooks.
Known CVEs in the ~80 production dependencies — including Electron, @ai-sdk/* packages, node-pty, better-sqlite3, and mustardscript — go undetected until a developer manually audits. New dependencies added by AI-assisted pull requests receive no automated security review. A supply-chain compromise in any transitive dependency would not be caught before release.
Remove --no-audit from npm ci invocations or add a dedicated step running npm audit --audit-level=high that fails CI on High/Critical findings with documented exceptions. Add a Dependabot configuration (.github/dependabot.yml) for both npm and GitHub Actions. Enable GitHub's Dependency Review Action on PRs. Add gitleaks or trufflehog to the pre-commit Husky hook for secret scanning. Consider adding CodeQL for JavaScript/TypeScript analysis.
08
— Security
MCP server credentials stored as plaintext in unencrypted SQLite
The mcpServers table's envJson and headersJson columns hold third-party API keys and auth headers as plain JSON. A comment in the debug handler explicitly excludes these fields noting 'may contain secrets', confirming first-party awareness.
Any process, forensic tool, or backup application that can read ~/.dyad/sqlite.db recovers all MCP server API keys in cleartext. On macOS/Windows, file-system access from a co-resident malicious app or a compromised cloud backup service is a realistic path to mass credential theft.
Encrypt envJson and headersJson values using the existing encrypt()/decrypt() helpers from src/main/settings.ts before writing to SQLite, storing the Secret envelope (value + encryptionType). Alternatively, migrate credential storage to the encrypted settings file and store only a non-sensitive reference ID in the database row.
09
— Documentation
Operational runbooks entirely absent for all release, incident, and recovery workflows
No repository document describes how to execute a release, respond to an incident, renew code-signing certificates, rotate provider credentials, restore a corrupted database, or recover the api.dyad.sh update service after a failure.
An unplanned event — certificate expiry blocking macOS releases, a bad auto-update manifest reaching all users, a corrupted user database, or a compromised API key — requires the team to reconstruct the response procedure from memory or source code. INF-001, INF-003, DAT-002, and INF-004 each document specific risks where the absence of a runbook directly extends mean-time-to-recovery. For api.dyad.sh, the rebuild path is entirely outside the repository, so a service loss requires institutional knowledge not captured anywhere.
Create an ops/ or docs/runbooks/ directory with at minimum: (1) a RELEASING.md covering the workflow_dispatch trigger, dry-run validation, asset verification, and promotion from beta to stable; (2) a CERT-RENEWAL.md covering Apple Developer ID and Azure Trusted Signing renewal timelines and steps; (3) an INCIDENT.md covering how to pull logs, disable a feature via update-server manifest, communicate to users, and roll back a release; (4) a RESTORE.md covering how to locate and restore from userData/backups/; and (5) an ops/api-dyad-sh.md documenting the update service hosting, deployment, and recovery path. These do not need to be long — clear bullet-point SOPs are sufficient.
10
— Code Quality
Multiple handler files exceed 1,500 lines
Four handler files—chat_stream_handlers.ts (2,196 lines), app_handlers.ts (2,029 lines), local_agent_handler.ts (1,929 lines), and github_handlers.ts (1,466 lines)—each contain hundreds of logical operations without internal file-level decomposition.
Files of this size are difficult to navigate, test in isolation, and review safely in PRs. Critical behaviors (auth flows in github_handlers, app lifecycle in app_handlers) are buried in large scrollable files, increasing the probability of introduced bugs going undetected.
Decompose each file by responsibility: e.g., split app_handlers.ts into app_crud_handlers.ts, app_process_handlers.ts, and app_screenshot_handlers.ts. Extract sub-functions to separate modules and add unit tests for each new module.
11
— Code Quality
Unused-variable checks disabled and widespread `any` usage despite strict mode
`noUnusedLocals` and `noUnusedParameters` are explicitly set to `false` in tsconfig.app.json. Combined with 668 `any` usages across 202 source files (including 129 `as any` casts), dead code accumulates silently and type safety gaps are pervasive.
Dead code silently accumulates, making refactoring risky. `any` casts at external API boundaries (e.g., `(await response.json()) as any` in github_handlers.ts) mean that GitHub API shape changes cause silent runtime failures rather than type errors.
Enable `noUnusedLocals: true` and `noUnusedParameters: true` incrementally with a CI gate. Audit `as any` casts at external API call sites (GitHub API, Supabase client) and replace with Zod parse or typed response schemas. Prioritize the github_handlers.ts and supabase_management_client.ts files.
12
— Architecture
All long-running work runs in the main-process event loop without queuing or isolation
LLM streaming, multi-step agent tool loops (up to DEFAULT_MAX_TOOL_CALL_STEPS steps, each with file I/O and potential shell execution), Supabase/Neon API calls, and git operations all run as async chains in the same Node.js main process. There is no per-chat serialisation, back-pressure, or worker thread isolation.
A slow LLM response or a large agent run delays all other IPC operations (app starts, settings reads, etc.) on the same event loop. Concurrent agent runs across multiple apps share the event loop with no throttling, increasing the risk of contention and making performance degradation hard to diagnose.
Move multi-step agent runs to dedicated worker threads using Node.js worker_threads (a pattern already used for the TSC worker and sandbox worker). Add per-chat concurrency guards so a second stream invocation on the same chat is rejected or queued. Instrument event-loop lag to make contention visible.
13
— Architecture
Backup runs only at startup and does not checkpoint the WAL before copying
BackupManager.initialize() is called once during app.whenReady(). It copies the SQLite database file directly without first issuing a WAL checkpoint (PRAGMA wal_checkpoint(FULL)), so the backup may exclude transactions that are in the WAL file but not yet merged into the main database file.
Users restoring from the most recent backup may lose transactions committed since the last OS-level WAL flush. In practice this window can span the entire session between launches.
Call db.$client.pragma('wal_checkpoint(FULL)') before copying the database file in BackupManager. Also schedule an additional backup on clean shutdown (before-quit) so the final session state is captured.
14
— Architecture
OAuth deep-link handler lacks state/PKCE validation before storing tokens
The dyad:// deep-link handler for Supabase, Neon, and Dyad Pro returns validates only that token fields are non-empty before writing them to encrypted settings. There is no PKCE verifier, OAuth state parameter, or nonce check.
A malicious application on the same machine can craft a dyad://supabase-oauth-return?token=... URL and trigger Dyad to overwrite its stored OAuth credentials. On macOS/Windows this requires the attacker to be able to invoke the registered URL scheme handler, which is possible from any local process.
Generate and store a cryptographic state parameter before initiating OAuth; verify it matches on return. For GitHub Device Flow (already stateless by design) this does not apply, but for Supabase, Neon, and Dyad Pro redirects, add state validation before committing tokens to settings.
15
— Data
Backup only triggers on version upgrades — entire session data unprotected between releases
BackupManager.initialize() compares the stored last-run version to the current app version and creates a backup only when they differ; a user who runs the same version across multiple sessions accumulates committed data with no backup protection until the next release.
Database corruption, accidental deletion, or a botched manual migration during a stable release cycle leaves the user with no recovery path. The 3-backup retention cap also means upgrade-triggered backups rotate out quickly, limiting point-in-time recovery windows.
Add a backup on clean shutdown (app before-quit event) to capture the final session state. Optionally add a periodic backup (e.g., once per day of active use) stored separately from upgrade backups. Document a restore procedure in the app or README so users know how to recover.
16
— Data
Compaction summary insert and chat record update are not wrapped in a transaction
performCompaction() calls db.insert(messages) to write the summary and then db.update(chats) to set compactedAt and pendingCompaction=false as two separate awaited operations; if the second write fails, the chat retains pendingCompaction=true and will trigger compaction again on the next message send.
A transient DB error between the two writes leaves a duplicate summary message in the chat on the next compaction cycle. While original messages are preserved, duplicate summaries corrupt the LLM's context window view and can cause off-by-one errors in the compaction boundary logic.
Wrap both writes in a single db.transaction() call: insert the summary message and update the chat record atomically. If the transaction fails, the catch block's clearPendingCompaction() call should also run inside the transaction or be kept outside as a best-effort cleanup.
17
— Data
safeStorage encryption silently falls back to plaintext with no user warning
The encrypt() function in settings.ts returns a plaintext Secret when safeStorage.isEncryptionAvailable() returns false — a condition that occurs on headless Linux systems and some CI environments — without surfacing any notification to the user.
Users on affected platforms believe their API keys and OAuth tokens are encrypted while they are stored as readable strings in user-settings.json. Any process or file-system read on the same machine can extract all credentials without privilege escalation.
On app startup, check safeStorage.isEncryptionAvailable() and display a persistent warning banner in the UI when encryption is unavailable, advising the user that secrets are stored in plaintext and recommending they avoid entering sensitive credentials. Consider blocking credential entry entirely on platforms where encryption is unavailable.
18
— Security
Indirect prompt injection via web_fetch and web_crawl tool results
The web_fetch and web_crawl tools return raw external HTML or markdown to the AI model with only triple-backtick sanitization applied; a malicious page can embed hidden AI instructions that enter the model's context alongside the legitimate system prompt.
If a user asks Dyad to clone or reference a page under attacker control, injected instructions in the page content could cause the AI to write backdoor files (write_file tool), exfiltrate .env or secrets files (read_file), execute malicious SQL against the connected Supabase/Neon database (execute_sql), or call external URLs to exfiltrate context (web_fetch). All of these tools have consent controls, but a convincing getConsentPreview message fabricated by the injection can mislead a user into clicking through.
Wrap all tool result content that originates from external sources (web_fetch, web_crawl, read_file for files the AI found via search) in a clearly labeled untrusted-content fence in the model context, with an explicit instruction that the content cannot override system-level directives. Consider marking web-crawl results as untrusted by prefixing them with a fixed sentinel string and adding a system prompt rule that content after the sentinel cannot issue tool calls. Apply URL validation that blocks private IP ranges (RFC1918, loopback, link-local) for the engine-side SSRF exposure.
19
— Security
AI_RULES.md from the active project is embedded in the system prompt without sanitization
The local agent prompt unconditionally reads and injects the project's AI_RULES.md into the system prompt as authoritative context; any project shared with the user that contains adversarial instructions will have those instructions treated as system-level guidance.
A third party sharing a project (repository, zip, or template) with a weaponized AI_RULES.md can pre-program Dyad to perform actions on the user's behalf: modifying files, executing SQL against connected databases, or exfiltrating data via web_fetch. Because the instructions appear to come from the system prompt rather than user chat, model-level trust hierarchies may not flag them.
Load AI_RULES.md into the user-role message context rather than the system prompt, or clearly label it as user-supplied project context within the system prompt with an explicit instruction that it cannot override core security directives. Display a warning when Dyad opens a project it has not seen before that AI_RULES.md will be read. Consider adding a max-length cap and a content filter that blocks known injection patterns (ignore all previous instructions, you are now, etc.).
20
— Security
No rate limiting or cost controls on LLM API calls
The local agent handler invokes AI SDK streaming with no per-session request count, token budget, or spend cap. There are no guards against a runaway agent loop consuming the user's API credits.
A prompt injection that causes repeated tool-call loops, or a bug in the agent step counter, can result in unbounded API spend against the user's OpenAI/Anthropic/Google key. DEFAULT_MAX_TOOL_CALL_STEPS limits tool calls within a single run but does not cap token count, parallel runs, or aggregate spend. Users cannot set per-session budgets.
Add a per-run token budget derived from user settings (default: a conservative ceiling). Expose a UI control for the user to set a per-run token limit. Implement exponential-backoff retry with a max-retry count for provider rate-limit errors. Emit a telemetry event and surface a warning when a run exceeds a configurable token threshold.
21
— Security
safeStorage encryption falls back to plaintext with no user notification
The encrypt() function in settings.ts silently returns a plaintext Secret when safeStorage.isEncryptionAvailable() returns false — a condition that occurs on headless Linux and some CI environments.
Users on affected platforms believe their API keys and OAuth tokens are encrypted while they are stored as readable strings in user-settings.json. Any co-resident process or filesystem reader can extract all credentials without privilege escalation. The same condition applies in IS_TEST_BUILD mode, which is relevant if test builds are distributed.
On app startup, call safeStorage.isEncryptionAvailable() and display a persistent warning banner when encryption is unavailable, advising the user that secrets are stored in plaintext and recommending they avoid entering sensitive credentials. Consider blocking credential entry entirely on platforms where encryption is unavailable. Remove the plaintext path from IS_TEST_BUILD and use a test-only key derivation instead.
22
— AI Engineering
`gh pr merge` is auto-approved despite being classified as blocked in the permission policy
`gh-permission-hook.py` includes `merge` in its PR-modification allow-list and `settings.json` lists `Bash(gh pr merge:*)` as auto-approved, contradicting `permission-policy.md` which classifies PR merging as RED/blocked and states 'user will do this manually'.
AI agents can merge their own PRs without any human approval prompt, including bot-authored PRs from automated fix or push workflows. This bypasses the intended review gate for every branch that goes through the `fix-issue` or `pr-push` skill chains.
Remove `merge` from the `pr_allowed_patterns` list in `.claude/hooks/gh-permission-hook.py` (line 588). Remove `"Bash(gh pr merge:*)"` from `.claude/settings.json`. Optionally update `permission-policy.md` to confirm RED status. If autonomous merges are intentional for certain bot workflows, gate them behind an explicit user-approved session variable rather than a blanket allow.
23
— AI Engineering
`Edit` and `Write` permissions are path-unrestricted in agent settings
`.claude/settings.json` grants `"Edit"` and `"Write"` at the top of the allow-list without path constraints. The carefully layered Bash allow-list and pre-tool-use hooks protect shell commands, but file modification tools are entirely unrestricted.
AI agents can silently modify security-sensitive files — `.github/workflows/*.yml`, `.claude/settings.json`, `.claude/hooks/*.py`, `CONTRIBUTING.md` — without any approval prompt. A prompt-injection or confused-agent scenario could escalate privileges by modifying the hook code or workflow permissions.
Add a pre-tool-use hook for `Edit` and `Write` tools that checks the target path against a deny-list of sensitive locations (`.github/workflows/**`, `.claude/settings.json`, `.claude/hooks/**`, `package.json`). Alternatively, add explicit scoped entries like `"Edit(.github/workflows/**)"` to the deny list with a matching hook that requires approval.
24
— AI Engineering
PR delivery documentation template omits security, rollback, and data-change coverage
The `pr-push` skill creates PRs with only a one-to-three-bullet summary and a test plan section. Security implications, IPC contract changes, data model changes, known limitations, and rollback notes are never prompted by the template.
AI-generated PRs are merged without a reliable audit trail for risk. A reviewer cannot tell from the PR body whether security-sensitive paths, breaking IPC contracts, schema migrations, or deployment sequencing issues are involved. Post-merge incidents become harder to attribute and reverse.
Extend the `pr-push` PR body template with conditional sections: 'Security/auth changes (if any)', 'IPC contract or data model changes (if any)', 'Known limitations or follow-up risks', and 'Rollback notes'. Add a prompt step that checks whether a matching `plans/*.md` exists and links it in the PR. Updating `.claude/skills/pr-push/SKILL.md` at lines 154-165 is the minimal change.
25
— Infrastructure
Auto-update server at api.dyad.sh is an undocumented black box with no recovery path
All Dyad installations that have auto-update enabled poll api.dyad.sh every 60 minutes for new releases; the service is referenced in src/main.ts but no IaC, deployment documentation, hosting provider identity, access control policy, or recovery procedure exists anywhere in the repository.
If api.dyad.sh becomes unavailable — due to hosting provider failure, domain lapse, credential rotation, or ownership change — all auto-updating users stop receiving updates with no user-visible explanation. Because the rebuild path lives entirely outside the repo, restoring the service after an incident requires out-of-band institutional knowledge. A misconfiguration of the update endpoint (e.g., serving a bad update manifest) would push to all stable or beta users before any monitoring could detect it.
Document the update server's hosting provider, architecture, deployment process, and owner in a RUNBOOK.md or ops/ directory in the repo. Express its configuration as IaC (Terraform, Pulumi, or the provider's native config-as-code) and store it in version control. Add an uptime monitor (e.g., BetterStack, Checkly) for the /v1/update/stable and /v1/update/beta endpoints and route alerts to at least one maintainer. Define an incident response step that points the updateSource host to a fallback URL (such as the GitHub Releases API directly) if the primary service is unreachable.
26
— Infrastructure
Release pipeline verifies asset presence but performs no functional smoke test
The verify-release-assets step in the release workflow calls scripts/verify-release-assets.js, which checks that all eight expected file names appear in the GitHub Release draft; it does not download any installer or verify that the distributed binary actually launches.
A build regression that produces a file of the correct name but incorrect format (corrupt ASAR, failed code-signing, wrong architecture slice) would pass asset verification and be published to all auto-updating users. On macOS, Gatekeeper would silently quarantine the app for affected users without a clear error. On Windows, SmartScreen may block a binary whose signing chain is invalid. Discovery would depend on user reports rather than automated detection.
Add a post-publish smoke-test job to the release workflow that downloads each platform installer from the published GitHub Release, installs it in a headless or sandboxed environment, launches the app with a --smoke-test flag (or checks for a known log line), and fails the workflow if the app does not start within a timeout. For macOS, use spctl --assess to programmatically verify the Gatekeeper acceptance of the notarized DMG/ZIP before publishing. Gate the 'publish' step on passing smoke tests for at least the primary platforms (macOS ARM64 and Windows x64).
27
— Observability
PostHog exception sampling drops 90% of crashes for non-Pro users
The PostHog before_send hook in renderer.tsx applies a 10% sampling pass for non-Pro users; only events that shouldBypassNonProTelemetrySampling returns true for — $exception events and events with error/error-type properties — are exempt from the 90% drop rate.
Most crash events and $exception events from the majority of the user base are dropped before reaching PostHog. A regression that crashes Dyad for non-Pro users would appear as a minor uptick in the PostHog dashboard rather than a spike, delaying discovery until multiple users file GitHub issues. The team cannot reliably distinguish between a widespread crash regression and a rare edge case using current telemetry.
This is a Dyad desktop app built on Electron with a Node.js + React renderer stack. Add Sentry (`@sentry/electron`) for dedicated error tracking — it captures both main-process and renderer errors with native stack traces, release tagging, and environment separation without requiring the PostHog sampling workaround. The Sentry free tier (5,000 errors/month) is sufficient for an open-source desktop app. Keep PostHog for usage analytics and route all exception capture to Sentry, which supports alert rules, issue grouping, and regression detection out of the box.
28
— Observability
Logs are unstructured strings with no correlation IDs, no remote retention, and no search capability
All 130+ source files that import electron-log emit string-interpolated messages (e.g., `IPC: ${channel} called with args: ${JSON.stringify(args)}`) with no JSON schema, no chatId/appId/requestId correlation field, and no remote shipping — the log file exists only on the user's local machine.
When a user reports a bug, the team cannot search logs by chat ID, app ID, provider, or error code across any request boundary. Reproducing a reported issue requires the user to manually locate their electron-log file and share it, and even then the logs cannot be correlated across multiple IPC calls for the same operation. Regressions in specific AI provider paths, compaction flows, or agent tool chains are invisible until users report them.
This is a Node.js Electron app. Replace ad-hoc string interpolation in electron-log with structured JSON log entries by setting `log.transports.file.format = '{level} {text}'` and logging plain objects rather than template strings. Add a `chatId`, `appId`, and `requestId` field to any log emitted inside a chat or agent operation. To make logs searchable beyond the local file, integrate Better Stack (Logtail) using the `@logtail/electron` or a generic HTTP transport — the free tier (1 GB/month) covers a typical open-source desktop user population for error-level logs only. Even partial adoption (structured logging for the agent handler and chat stream handler) would dramatically improve incident diagnostics.
29
— Observability
Auto-update server api.dyad.sh has no uptime monitoring or health endpoint
Every Dyad installation polls https://api.dyad.sh/v1/update/{stable|beta} every 60 minutes; no uptime monitor, no health endpoint reference, and no alert route for this service exists anywhere in the repository.
An outage of api.dyad.sh — due to hosting provider failure, domain lapse, or misconfiguration — would go undetected by the team until users begin reporting that auto-update is broken or that a bad update manifest was served. Because the rebuild path is undocumented (INF-001), recovery time would be further extended.
Add a Better Stack Uptime or UptimeRobot free-tier monitor for both https://api.dyad.sh/v1/update/stable and https://api.dyad.sh/v1/update/beta (3-minute check intervals, email alert to at least one maintainer). Implement a /health endpoint on api.dyad.sh that returns a 200 with a JSON body and monitor that rather than the update path if the update endpoint returns non-200 for valid manifest requests. Pair the uptime monitor with the IaC and runbook work in INF-001.
30
— Observability
No LLM observability — token usage, provider latency, and model error rates are invisible across the user base
Token counts are written to messages.maxTokensUsed in SQLite but never emitted to PostHog or any telemetry destination; provider API errors (rate limits, model failures) are explicitly filtered from telemetry in shouldFilterTelemetryException; no model, provider, or latency fields appear in any PostHog event.
The team cannot determine which AI providers are failing most frequently, which models are consuming the most tokens, whether a provider degradation is causing user-visible errors, or whether the DEFAULT_MAX_TOOL_CALL_STEPS cap is commonly hit. When a provider rolls out a breaking change (e.g., a model deprecation or API schema change), discovery depends entirely on user-filed GitHub issues rather than a detectable signal.
Emit a PostHog event (or Sentry breadcrumb) on each chat completion with non-identifying fields: provider name, model name (no API key), token count (promptTokens, completionTokens), finish reason, and whether an error or retry occurred. Filter out any user-identifiable content. This gives enough signal to detect provider degradation, model-change regressions, and token-budget problems without logging prompts or outputs. Also remove the blanket suppression of RateLimitError (429) from shouldFilterTelemetryException — replace it with a sampled telemetry event so rate-limit spikes are visible.
31
— Delivery
Windows CI build skips lint, type-check, and unit-test gates
In the CI workflow's 'build' job, the presubmit (oxlint + oxfmt), TypeScript check (tsgo), and Vitest unit tests are all gated on `contains(matrix.os.name, 'macos')` — the Windows matrix entry only runs the Electron Forge package build and uploads the artifact for E2E use.
A TypeScript error, lint violation, or failing unit test introduced in a Windows-conditional code path or platform-sensitive module can be merged without detection. Because the Windows build artifact feeds the Playwright E2E suite, a build that passes macOS quality gates but fails Windows-specific checks still produces a deployable artifact.
Either extend the quality-gate conditions to run on all matrix OS entries (accepting additional runner minutes), or add a documented policy that macOS results are authoritative for cross-platform TypeScript code and require Windows-specific paths to have unit tests. At minimum, add a tsgo type-check step for Windows that does not rely on the macOS conditional.
32
— Delivery
LLM agent eval suite is not wired into CI
vitest.eval.config.ts defines an eval suite targeting src/__tests__/evals/**/*.eval.ts and is reachable via `npm run eval`, but no GitHub Actions workflow references this script — it is never executed as part of CI or the release pipeline.
Regressions in AI agent behavior — prompt changes, tool-call schema shifts, agent output formatting, or model-provider configuration changes — are never caught by automated checks before release. Because Dyad's core value proposition is the local AI agent, undetected behavioral regressions in the agent path directly degrade the product for all users on every release.
Add a dedicated eval CI job (or a scheduled nightly workflow) that runs `npm run eval` against a stubbed or canned LLM response set (e.g., using the existing testing/fake-llm-server) so agent behavior is regression-tested without live API calls. Gate the eval job on agent-related file changes (src/pro/main/ipc/handlers/local_agent/**, src/prompts/**) to keep CI cost manageable. At minimum, run the eval suite on pushes to main so regressions are detected before the next release.
33
— Delivery
Codex PR review installs @openai/codex@latest without version pinning in a privileged pull_request_target context
codex-pr-review.yml runs on pull_request_target (which executes in the base repository context with access to org secrets) and contains `npm install -g @openai/codex@latest` — resolving whichever npm package version is current at workflow execution time.
If the @openai/codex package is compromised or an unvetted major version is published, the malicious code executes with access to CODEX_AUTH_JSON, CODEX_AUTH_JSON_1, CODEX_AUTH_JSON_2, and DYAD_GITHUB_APP_PRIVATE_KEY secrets. Because pull_request_target runs in the privileged base-repo context, a supply-chain compromise here has broader reach than a standard pull_request workflow.
Pin the Codex CLI to a specific version (`npm install -g @openai/codex@0.x.y`) and update it intentionally via a reviewed PR. Add a Dependabot entry for this package or create a scheduled workflow to check for new versions. Alternatively, install Codex from a checked-in lockfile or use a pre-built GitHub Actions action with a pinned SHA digest rather than installing from npm at runtime.
34
— Delivery
No feature flag or kill-switch mechanism for production releases
No feature flag system (LaunchDarkly, Unleash, GrowthBook, Statsig, ConfigCat, Postgres flags table, or environment-driven flags) is present anywhere in the codebase. The only rollout control is the stable/beta release channel split via update-electron-app.
When a defective build reaches the stable auto-update channel, all users with auto-update enabled receive it within 60 minutes of the next app restart. There is no server-side mechanism to stop propagation, roll back to a previous release manifest, or disable a specific feature without publishing a hotfix release and waiting for it to propagate. For AI-adjacent features (agent tool calls, file writes, shell execution), a bad release can cause irreversible user-data changes before a fix is available.
Add a lightweight server-side kill-switch to api.dyad.sh: a JSON endpoint that the Dyad client checks on startup, containing a minimum required version and a list of disabled feature keys. If the running version is below minimum, the app can surface a 'please update' banner; if a feature key is disabled, the corresponding UI entry point is hidden. This requires no third-party tooling — a static JSON file served from the update server is sufficient. For higher granularity, evaluate GrowthBook (open-source, self-hostable) wired to a feature-flag table in the existing SQLite schema.
35
— Delivery
No CI migration replay gate; db:push script can bypass migration tracking
None of the GitHub Actions workflows includes a step that replays all Drizzle migrations against a fresh SQLite file to verify sequencing. Additionally, package.json exposes a `db:push` script (drizzle-kit push) that applies the current schema state directly, bypassing the ordered migration chain.
A migration that references a column or table introduced in a later migration (out-of-order numbering) would not be caught before release. On first launch after upgrade, affected users would encounter a startup crash with no recovery path other than restoring from backup. The db:push script, if accidentally invoked in a local-dev workflow, can leave the developer's schema ahead of the migration sequence, producing false CI passes when the actual migration sequence would fail.
Add a CI step (in the build or a separate job) that runs `npx drizzle-kit migrate` against a temporary SQLite file from scratch and verifies the resulting schema matches the Drizzle schema definition. Remove or clearly warn against db:push in package.json (rename to db:push:danger or add a safety check script). Cross-reference DAT-007 for the related down-migration gap.
36
— Documentation
README.md is a marketing page that does not surface the contributor or AI-agent entry points
The repository README links to CONTRIBUTING.md and docs/architecture.md but does not mention AGENTS.md, rules/, plans/, or .claude/README.md — the resources that actually guide code contributions, especially for AI-assisted workflows.
A new human contributor opening the repository sees only marketing content (features, download links, community, license) with no direct path to the developer setup guide. An AI assistant (Claude Code, Cursor) starting from the repo root will not discover AGENTS.md from the README, causing it to miss the rules index and the pre-commit requirements until a human points it to CONTRIBUTING.md. Both cases increase onboarding friction and the likelihood of ignoring project conventions.
Add a 'Contributing / Development' section to the README (before the License section) that links to CONTRIBUTING.md for human contributors and AGENTS.md for AI agents, and briefly notes the Electron + TypeScript + SQLite stack and the requirement to create userData/ before running. A two-paragraph summary with links is sufficient — the full detail lives in CONTRIBUTING.md and AGENTS.md.
37
— Documentation
docs/architecture.md and docs/agent_architecture.md coexist without explaining which system applies or how they relate
docs/architecture.md describes the XML-tag (dyad-*) approach as Dyad's primary system; docs/agent_architecture.md says 'Previously, Dyad used a pseudo tool-calling strategy... Now that models have gotten much better with tool calling...' — implying the XML approach is superseded — but both systems are active concurrently for different product modes.
A contributor tasked with modifying chat processing cannot determine from documentation alone whether to edit the XML response processor (src/ipc/processors/response_processor.ts) or the Pro local agent handler (src/pro/main/ipc/handlers/local_agent/). A new AI agent working on a feature will pick the wrong code path without the rules/local-agent-tools.md hint in AGENTS.md. The docs create the impression of an architecture migration that has replaced the old system, which is not accurate.
Add a 'Current System State' section to docs/architecture.md that explains: (1) the XML-tag/dyad-* approach remains the primary mode for all users; (2) the Pro local-agent tool-calling mode (described in docs/agent_architecture.md) is a separate mode enabled by Pro; and (3) both can be active simultaneously depending on the user's mode setting. Update the 'Previously... Now...' framing in docs/agent_architecture.md to 'In addition to the classic XML-tag mode...' to eliminate the false supersession narrative.
38
— Documentation
All three ADRs describe proposed future cloud features; no ADRs document the deployed system's key decisions
ADR-0001, ADR-0002, and ADR-0003 are all marked 'Proposed' and document a future cloud/web/mobile platform that has not been built. The key architectural decisions that shape the actual deployed system — SQLite over Postgres, Drizzle ORM, electron-forge, the Zod-validated IPC contract pattern, PostHog, and the Vercel AI SDK abstraction — have no decision records.
Contributors and AI agents cannot find the rationale for the existing system's structure. Decisions like 'why is the IPC layer contract-driven with Zod?' or 'why SQLite instead of a remote database?' have answers only in informal FAQ sections or are entirely implicit. Without decision records for existing choices, future contributors may re-litigate settled questions, propose changes that contradict the architecture's intent, or miss critical constraints (e.g., 'no remote database because local-first is a core product principle').
Write ADRs for the four to six decisions that most affect contributor work: (1) Electron + IPC contract pattern (with Zod), (2) SQLite + Drizzle for local persistence, (3) XML-tag vs. tool-calling for AI interaction (and the coexistence rationale), (4) PostHog for telemetry (opt-in, sampling rationale), (5) Vercel AI SDK as the LLM provider abstraction. These can be short (one page each). Update ADR-0001 through ADR-0003 statuses to 'Proposed — unimplemented' to avoid implying they describe the current system.
39
— Code Quality
Duplicated IPC handler validation infrastructure
`createTypedHandler` and `createLoggedTypedHandler` in base.ts share near-identical Zod input validation and output validation logic, diverging only in logging calls.
Any bug fix or improvement to the validation or error-reporting logic must be applied in two places. The duplication is modest but the two functions are the foundation of all IPC type safety.
Extract the shared validation core into a private `validateAndExecute` helper, then implement both public functions as thin wrappers that pass a logger parameter or no-op. This reduces the shared logic to a single place.
40
— Architecture
All provider credentials share a single flat settings keyring
Every API key, OAuth token, and provider credential is stored in a single user-settings.json file, encrypted as a unit by Electron safeStorage. There is no per-credential isolation or revocation granularity.
Any code path that reads or logs settings exposes all credentials simultaneously. Rotating one provider's key requires touching the whole settings object. If safeStorage is unavailable on a platform (or the OS keychain is bypassed), the entire credential store is exposed.
Split provider credentials into per-provider encrypted entries, either using separate files or a keyed store within the settings structure. This limits the blast radius of a partial exposure and enables per-credential rotation without touching other providers.
41
— Architecture
Agent runs are purely in-memory with no crash-resume capability
The local agent handler maintains all run state (current step, pending tool calls, accumulated messages) in memory. A forced quit, renderer crash, or OS kill during a multi-step run loses this state entirely.
Mid-run crashes leave apps in partially modified states (some files written, some not) with no indicator of what was completed. Users must manually inspect file state and git log to understand what happened. For long agent runs this is disorienting and error-prone.
Persist agent run state to a dedicated SQLite table (step index, tool calls in flight, accumulated AI messages JSON) after each completed step. On next launch for the same chat, detect and surface the incomplete run, allowing users to resume or discard.
42
— Architecture
IPC output validation is disabled in production
createTypedHandler and createLoggedTypedHandler only validate handler return values against the contract output schema in NODE_ENV=development. Production builds skip output validation entirely.
Contract violations (handler returns a shape that does not match its declared output schema) surface as runtime type errors in the renderer rather than as caught, categorised, and telemetried schema failures. This makes regressions harder to detect and attribute.
Enable output validation in production with a non-throwing path: log the schema mismatch via sendTelemetryException, return the value anyway (fail-open), and increment a counter. This makes violations visible in PostHog without breaking the handler call.
43
— Data
No unique constraint on apps.path allows duplicate app registrations
The apps table has no unique constraint on the path column, so the same filesystem directory can be imported multiple times, creating separate app records with divergent chat histories pointing at the same on-disk repository.
Duplicate records result in split chat histories for the same codebase, agent runs that operate on the same files under different app identities, and confusing UI state. Deleting one duplicate (cascades chats/messages/versions) does not remove the other, leaving dangling references in the user's mental model.
Add a unique constraint on apps.path in a new Drizzle migration. Before applying the constraint, deduplicate existing rows by merging chats to the oldest app record for each duplicate path and deleting the extras.
44
— Data
aiMessagesJson written to SQLite without runtime schema validation
The messages.aiMessagesJson column is declared as TypeScript type AiMessagesJsonV6 but the value is written directly from the AI SDK without a Zod parse; a version mismatch or malformed SDK response can persist an invalid envelope to the database.
A corrupted aiMessagesJson row surfaces as a runtime error when the agent or compaction logic reads back the message array, potentially breaking entire chat sessions. The sdkVersion field (ai@v6) is present for versioning but not enforced at write time, making silent schema drift possible on SDK upgrades.
Define a Zod schema for AiMessagesJsonV6 (matching the ModelMessage array structure from the AI SDK) and call schema.parse() before any db.insert or db.update that sets aiMessagesJson. Log a structured error and skip persistence (or fall back to null) if validation fails rather than crashing the handler.
45
— Data
No down-migration scripts — rollback requires full backup restore
All 30 Drizzle migrations are purely additive (ALTER TABLE ADD COLUMN, CREATE TABLE, CREATE INDEX) with no corresponding down or rollback scripts in the drizzle/ directory.
If a migration introduces a schema regression, the only recovery path is restoring from the backup taken at the previous version upgrade. Users who made substantial data changes after upgrading would lose all post-upgrade data on rollback. The db:push script in package.json, if accidentally run, can apply schema changes outside the migration sequence with no rollback path.
For future migrations that delete columns or change column types, author a corresponding rollback migration and document the rollback procedure. Remove or rename db:push to prevent accidental use against a production database. Add a CI step that replays all migrations against a fresh SQLite file to catch sequencing errors before release.
46
— Security
HTTP MCP server transport accepts plain HTTP URLs without HTTPS enforcement
The HTTP transport branch of McpManager passes the stored URL directly to StreamableHTTPClientTransport with no protocol check; an MCP server configured with http:// will have all tool calls and credential headers sent in cleartext.
On shared local networks (public Wi-Fi, corporate LAN without full TLS inspection) an attacker can intercept MCP tool results and inject fabricated responses, or capture the Authorization headers sent in headersJson. This also allows a deep-link installer to point Dyad at a legitimate-looking http:// MCP service that is actually an attacker-controlled HTTP server.
In McpManager.getClient(), validate that s.url starts with https:// before constructing StreamableHTTPClientTransport and throw a DyadError for plain HTTP. In the MCP server creation handler, reject http:// URLs with a user-visible error. Optionally allow an explicit developer bypass flag for local testing.
47
— Security
Pre-commit hooks lack secret and credential scanning
The Husky pre-commit hook runs only lint-staged (formatting and lint checks). No tool scans staged files for API keys, tokens, or credentials before they are committed.
A developer or AI assistant (cursor, claude-code) that accidentally includes a hardcoded API key in a change will not be caught before the commit enters git history. For an open-source project distributed on GitHub, any leaked key in git history is exposed permanently unless the history is rewritten.
Add gitleaks or trufflehog as a lint-staged entry that runs against all staged files. Document the pre-commit setup in CONTRIBUTING.md and add a 'dev setup includes secret scanning' prerequisite to the onboarding guide. Consider enabling GitHub's built-in secret scanning push protection for the repository.
48
— AI Engineering
TypeScript prototype files in `plans/` can be mistaken for reviewed specs by AI agents
`plans/language-model-catalog-route.ts` and `plans/catalog-data.ts` are TypeScript source files committed alongside the spec Markdown files. The `swarm-to-plan` and `fix-issue` skills read `plans/` as AI-context for feature work.
AI agents may treat stale prototype code as authoritative implementation specs, implementing against obsolete type signatures rather than reviewed design intent. The mixed doc/code directory also makes it harder for humans to browse active specs.
Move TypeScript planning artifacts to `plans/_prototypes/` or delete them after implementation. Optionally add a note in the `swarm-to-plan` skill to skip non-Markdown files when reading the plans directory.
49
— AI Engineering
No durable link connects plans to their implementing issues and PRs
The 18 `plans/*.md` specs are comprehensive, but there is no mechanism stamping a plan with its implementing PR number, completion date, or implementation divergences. Completed plans remain undistinguished from active specs in AI context.
AI agents that read `plans/` as context may treat an already-implemented or superseded plan as an open spec, leading to duplicate or conflicting implementation. Post-implementation auditing (did the code match the spec?) requires manual cross-referencing.
Add a 'Status' field (Planned / In Progress / Implemented / Superseded) and a 'PR / issue' field to the plan template. When `pr-push` or `plan-to-issue` runs, check for a matching plan file and prompt the agent to update the status. Archive completed plans in `plans/_done/` so the root of `plans/` contains only active specs.
50
— Infrastructure
Code-signing certificate expiry is not monitored and has no documented renewal process
The release workflow depends on an Apple Developer ID certificate (imported via tools/add-macos-cert.sh) and an Azure Trusted Signing profile; neither has an expiry check, calendar reminder, or renewal runbook documented in the repository.
When the macOS Developer ID certificate expires, macOS Gatekeeper will reject all newly built installers as unsigned, blocking the release pipeline silently until a developer investigates the signing step failure. Apple Developer Program memberships expire annually; a lapsed membership also invalidates notarization. The Azure Trusted Signing profile has its own renewal cadence. Neither failure mode provides advance warning under the current setup.
Add a scheduled GitHub Actions workflow (monthly or quarterly) that calls the Apple Developer API or inspects the stored P12 certificate's notAfter field to emit an alert if expiry is within 60 days. Document the renewal steps for both the Apple Developer ID certificate and the Azure Trusted Signing profile in a RELEASING.md or ops runbook. Set a calendar reminder for the team tied to the membership renewal date.
51
— Infrastructure
No in-app restore path or documented recovery procedure for local backups
BackupManager creates versioned backups with SHA-256 checksums in userData/backups/ but provides no in-app restore UI, no CLI restore command, and no user-facing documentation explaining how to recover from a corrupted or lost database.
Users whose SQLite database is corrupted mid-session (between version upgrades, when no backup has been taken) or who upgrade and then need to roll back to a previous data state have no guided recovery path. The three-backup retention cap means a user who upgrades twice without noticing data loss will have rotated out the pre-corruption backup. Affected users must manually locate userData/, identify the correct backup directory, and overwrite the live database file — a procedure that is not documented anywhere accessible to non-technical users.
Add a Settings → Backups panel that lists available backups with timestamps and allows one-click restore (with a confirmation dialog). Document the manual restore procedure (path to userData/backups/, which files to copy) in the README or a Help menu link. Extend BackupManager to take a backup on clean app shutdown (the before-quit event) to reduce the recovery data gap, complementing the existing upgrade-triggered backups.
52
— Observability
Bug report logs submitted to public GitHub issues may contain chat content
The ErrorBoundary 'Report Bug' button in src/components/ErrorBoundary.tsx embeds the last 3,500 characters of the electron-log file into a pre-filled GitHub issue body and opens it in the browser; log entries from createLoggedHandler include raw JSON.stringify(args) for IPC args, which can include chat message content, file paths, and app names.
A user who clicks 'Report Bug' after a crash may inadvertently publish recent chat context — including prompts sent to the AI, file paths of their project, or app configuration data — to a public GitHub issue. For users working on proprietary codebases or sensitive projects, this is an unexpected data exposure.
Before embedding logs in the bug report body, apply a log scrubber that redacts any field value longer than 200 characters (which would typically be chat messages or file content) and replaces it with a [redacted-N-chars] placeholder. Alternatively, show the user the log content in a preview dialog with a toggle to include/exclude it before opening the GitHub issue. Also update createLoggedHandler to truncate args to a safe preview (e.g., the first 50 chars of each argument) rather than embedding the full JSON value.
53
— Delivery
No CHANGELOG or structured release notes in the repository
The repository has no CHANGELOG.md and no automated release-note generation step in the release workflow; version bump PRs use only a 'Bump to vX.Y.Z' commit message with no summary of included changes.
Users upgrading via auto-update have no structured way to understand what changed, what bugs were fixed, or whether any breaking behavior was introduced. Dependency reviewers and security auditors have no per-release inventory of functional changes. Over time this erodes user trust and increases support burden as users cannot self-diagnose whether a regression appeared in the current version.
Add a `release_notes` step to the release workflow that uses `gh release edit` to populate the GitHub Release body with a generated changelog (e.g., via conventional-changelog-cli or GitHub's built-in release-notes generation using PR labels). Alternatively, require version-bump PRs to include a CHANGELOG.md entry as part of the bump-version.mjs script, and have the release workflow read that entry into the GitHub Release body.
54
— Documentation
Backup restore procedure absent from all user-facing documentation
BackupManager creates versioned backups with SHA-256 checksums in userData/backups/ on version upgrades, but no README, in-app help, or contributor guide explains how a user locates or restores from these backups.
Users whose SQLite database is corrupted mid-session have no documented recovery path. CONTRIBUTING.md mentions deleting userData/sqlite.db to reset the database during development but does not mention the backup directory. Non-technical users who upgrade and then experience data loss after a bad migration cannot self-recover without filing a GitHub issue and receiving out-of-band guidance. This directly compounds the technical gap flagged in DAT-002 and INF-004.
Add a 'Data Recovery' section to CONTRIBUTING.md (and/or a short help page in the Settings UI) explaining: the location of userData/backups/ per platform (macOS ~/Library/Application Support/dyad/backups/, Windows %APPDATA%/dyad/backups/, Linux ~/.config/dyad/backups/), how to identify the correct backup by timestamp and version, and the safe copy procedure (quit app, copy sqlite.db from backup dir, relaunch). This is a two-paragraph addition that significantly reduces user support burden.