AnchorStack

bolt.diy Security & Engineering Audit

You shipped it.
Is it safe to operate?

We audited bolt.diy across 9 engineering areas - 76 findings, 27 of them critical or high. Here's the picture.

https://github.com/stackblitz-labs/bolt.diy

fail - Unsafe to operate3.5 of 10
2critical
25high
36medium
13low
Area Scores

9 areas evaluated

Each area scored 0-10 by an AnchorStack engineer.

Code Quality

4.5/10

The codebase has solid tooling foundations — TypeScript strict mode, ESLint, Prettier, and Husky pre-commit hooks — but critical paths are nearly untested (only 3 spec files in the entire app) and `as any` appears 156 times across 63 files, including at every provider API boundary and server-side trust boundary. A 22-provider architecture creates structural duplication in the fetch-and-map pattern and hardcoded model capability tables. CI quality checks (dead code, unused deps, complexity) all use `continue-on-error: true` and never block, and debug-level `console.log` statements were left in server-side production code. Changes to the LLM pipeline or any provider carry meaningful regression risk because there is no safety net.

failWeight x1

Architecture

4.8/10

bolt.diy is a single Remix application that proxies LLM requests server-side and runs a WebContainers sandbox entirely in the browser. The architecture is well-matched to the product goal (in-browser AI coding agent with no server-side file system) but carries two acute structural risks: long-running operations (Netlify/Vercel deployment polling loops of up to 60 seconds, LLM streaming up to 45 seconds) execute synchronously in HTTP request handlers with no queue or worker backing, creating hard timeouts on the Cloudflare Workers target; and an unauthenticated open-proxy route gives any caller the ability to route arbitrary HTTPS traffic through the server. State management is coherent for the client-only persistence model but offers no recovery path if the browser's IndexedDB is lost.

failWeight x1

Data

4.1/10

The data layer is intentionally 100% client-side: chat history and snapshots live in the user's browser IndexedDB, settings and connection tokens live in localStorage, and LLM API keys live in unencrypted cookies. No server-side database exists, and no recovery path exists if the user clears browser storage. The primary risks are sensitive-data exposure (API keys and OAuth tokens stored without encryption and included in plaintext settings exports), absence of any backup or recovery mechanism, O(n) full-scan ID generation that degrades with large chat histories, and non-atomic multi-step chat creation that creates race windows across concurrent browser tabs.

failWeight x1.5

Security

2.6/10

bolt.diy presents severe security risks for any deployment exposed to untrusted networks. The application has no authentication or authorization layer — every API route is publicly accessible, including an endpoint that exports all server-side LLM API keys in plaintext and another that accepts arbitrary MCP stdio server configurations, enabling unauthenticated remote code execution on the Docker host. Integration tokens for GitHub, GitLab, Vercel, Netlify, and Supabase are exposed to all browser users via VITE_-prefixed environment variables bundled into the client JavaScript. Rate limiting is trivially bypassable by spoofing the X-Forwarded-For header. Although the no-auth posture is a documented design decision for single-user self-hosting, the blast radius of the RCE and mass key-export issues requires critical remediation before any networked deployment.

failWeight x1.5

AI Engineering

2.3/10

bolt.diy is itself an AI engineering product — a multi-provider LLM coding agent — but it has almost no AI engineering practices applied to its own development workflow. There are no context files (no CLAUDE.md, AGENTS.md, .cursor/rules, or equivalent), no written repo tenets, no spec-driven development process, and no validation gates specifically requiring AI-generated changes to pass tests before merge. The product ships sophisticated in-context summarization and file-selection pipelines for end-users, but the team building it carries the risk of contributors making large, unreviewed prompt or system-prompt changes with no automated behavioral tests to detect regressions. The highest immediate risk is that the three overlapping system prompts (default, original, optimized/fine-tuned, discuss) can be silently changed and shipped with no behavioral gate; and that MCP servers added by end-users can execute arbitrary stdio commands with no documented approval model for contributors reviewing such integrations.

failWeight x1

Infrastructure

3.3/10

bolt.diy is a Docker-first, self-hosted application distributed as a GHCR image. Infrastructure artifacts (Dockerfile, docker-compose.yaml, CI workflows) are fully version-controlled and reproducible from the repo. However, the production runtime is `wrangler pages dev` — Wrangler's local development server — rather than a production-grade process, and the production Docker service carries no restart policy, meaning any crash (including via the unauthenticated RCE in SEC-002) leaves the deployment down until manual intervention. The container runs as root with no USER directive, and no image vulnerability scanning exists in the publish pipeline. There is no staging environment for server-side changes, no SOC2-grade infrastructure controls, and no container resource limits or LLM spend guardrails at the infrastructure layer.

failWeight x1

Observability

2.1/10

bolt.diy has near-zero production observability. No error tracking, uptime monitoring, alerting, or structured searchable logs exist for any deployment. The only telemetry artifacts are: a custom in-browser log store that persists to browser cookies (ephemeral, ~4 KB limit, no external destination), a minimal /api/health endpoint that always returns healthy regardless of dependency state, and container stdout output with no retention, structure, or search path. Server-side logs use a colorized console wrapper with no JSON structure, no request/correlation IDs, no environment or release tags, and no centralized destination. Any production failure — container crash, LLM provider outage, OOM kill, invalid API key — is invisible until a user manually notices and reports it. This is a critical gap for any operator running bolt.diy as a networked service.

failWeight x1

Delivery

4.8/10

bolt.diy has a functional GitHub Actions CI/CD pipeline covering build, test, Docker publish, preview, Electron, and release automation, which is solid for an open-source self-hosted project. However, several critical delivery gaps undermine release safety: the Docker publish workflow runs in parallel with CI tests, allowing untested code to land in GHCR :latest before the test job completes; there are no acceptance, e2e, or LLM-specific regression tests in CI; the feature flag system is a non-functional stub with a TODO comment; and the release mechanism relies on a magic commit-message string with no human approval gate. Rollback is partially addressed via SHA-tagged Docker images but has no documented runbook or hotfix process.

failWeight x1

Documentation

3.3/10

bolt.diy has a solid getting-started experience for end-users — README, .env.example, and the MkDocs site cover installation across all three deployment paths. Beyond that initial onboarding layer, the documentation posture collapses: there are no architecture docs describing the three incompatible deployment targets and their runtime constraints, no operational runbooks, no decision records, no agent/AI context files, and no internal API documentation for the 31 server routes. Duplicate FAQ and CONTRIBUTING files have diverged, with the root-level FAQ recommending stale model versions. A new contributor or AI-assisted maintainer cannot understand the system's critical constraints, respond to production incidents, or safely change core workflows without access to knowledge that exists only in PR history and tribal memory.

failWeight x1
Findings

Where to look first

01

criticalSEC-001Security

Unauthenticated endpoint exports all server-side LLM API keys

GET /api/export-api-keys requires no authentication and returns every LLM provider key found in Cloudflare env, process.env, and LLMManager env, merged with keys from the request cookie.

Impact

Any network-reachable client — including an anonymous internet user if the Docker port is exposed — can harvest all operator-configured API keys (Anthropic, OpenAI, Groq, Mistral, Google, etc.) with a single HTTP request. Key compromise is silent with no rotation mechanism.

Remediation

Restrict this endpoint to authenticated requests. For a self-hosted deployment, require a configurable admin secret (e.g., BOLT_ADMIN_TOKEN env var) verified in a server-side check before returning keys. Consider removing the endpoint entirely and providing keys only through the in-browser settings UI.

02

criticalSEC-002Security

Unauthenticated MCP config endpoint enables arbitrary OS command execution

POST /api/mcp-update-config accepts an MCPConfig body with no authentication. A stdio-type entry with an attacker-controlled command field is passed to Experimental_StdioMCPTransport, which spawns the command directly on the host OS.

Impact

Full remote code execution on the Docker container (and potentially the host via escape) is available to any unauthenticated caller who can reach the port. An attacker can exfiltrate all secrets, install persistent backdoors, or destroy the container state.

Remediation

Add authentication before this route executes any config update. At minimum, verify a shared secret header. Additionally, validate that the MCP command field matches an explicit allowlist of safe executables before spawning. Consider restricting MCP stdio to a sandbox user with no network access.

03

highCOD-001Code Quality

Critical LLM pipeline and persistence layer have no tests

All three spec files test utility parsing; the entire server-side LLM flow, 22 provider implementations, 33 API routes, and the IndexedDB persistence layer have zero test coverage.

Impact

Regressions in the chat pipeline, model selection, context summarization, or persistence are invisible until they reach users. A bug introduced in streamText(), selectContext(), or any provider's getDynamicModels() will not be caught before deployment.

Remediation

Add integration tests for the streamText() and selectContext() server functions using mocked provider responses. Add unit tests for each provider's getDynamicModels() response-mapping logic. Add tests for IndexedDB persistence helpers (setMessages, forkChat, duplicateChat) using fake-indexeddb. Enforce a minimum coverage threshold in CI (e.g., 60% on lib/.server/ paths).

04

highCOD-002Code Quality

Pervasive `as any` casts at every external API and server trust boundary

156 occurrences of `as any` exist across 63 files; every provider casts its fetch() response to `any`, and api.chat.ts types the `files` field from the request body as `any`.

Impact

TypeScript's safety net is bypassed exactly where it matters most: provider API responses, request body deserialization, and server environment bindings. A changed API response shape (e.g., Anthropic's models endpoint) will compile fine but throw at runtime. Silent data corruption is possible without any compile-time warning.

Remediation

Define typed interfaces for each provider's API response (e.g., AnthropicModelsResponse, OpenAIModelsResponse) and use Zod or manual guards to parse them. Replace `files: any` in api.chat.ts with the existing FileMap type. Replace `serverEnv as any` with the concrete Env type or use the BaseProvider.convertEnvToRecord() helper already available.

05

highARC-001Architecture

Blocking polling loops in synchronous HTTP handlers

The Netlify and Vercel deploy routes contain while-loops that poll an external API up to 60 times with 1-second delays, executing entirely within a single HTTP request handler.

Impact

On Cloudflare Workers the 30-second wall-clock limit causes every deploy to return a 524 timeout before completion. On Docker-Wrangler the thread is blocked for the full polling window, preventing any other requests from being served on that CPU context. Failed mid-poll deploys leave orphan sites on Netlify/Vercel with no cleanup path.

Remediation

Move deploy initiation to a fire-and-start pattern: the route creates the deploy and returns a deploy ID immediately; the client polls a lightweight `/api/deploy-status/:id` route for status. Alternatively, accept a Netlify/Vercel webhook and store deploy state server-side. Remove all `await new Promise(resolve => setTimeout(resolve, 1000))` calls from route action handlers.

06

highARC-002Architecture

Unauthenticated open-proxy route with no domain allowlist

The git proxy route (`api.git-proxy.$`) extracts the target domain from the URL path and proxies any HTTPS request with `Access-Control-Allow-Origin: *`, requiring no authentication and enforcing no domain restrictions.

Impact

Any browser or third-party client can use the deployed instance as a free open CORS proxy to reach arbitrary HTTPS endpoints, including internal network addresses reachable by the container. This enables SSRF, credential exfiltration via redirects, and abuse of the server's egress bandwidth/IP reputation. The `redirect: 'follow'` option compounds the SSRF risk.

Remediation

Add an allowlist of permitted target domains (e.g. `github.com`, `gitlab.com`). Require a signed or session-authenticated request. Change `redirect: 'manual'` or restrict followed redirects to the same allowlist. Log and rate-limit proxy requests per session.

07

highARC-003Architecture

LLM stream recovery timeout exceeds Cloudflare Worker limits

The chat action creates a `StreamRecoveryManager` with a 45-second timeout and up to 2 retries, keeping the streaming response open for potentially 135 seconds in the worst case.

Impact

Cloudflare Workers terminates requests at 30 seconds of wall-clock time (or earlier under CPU limits). Every stream recovery attempt silently fails at the platform level while the client sees a dropped connection. In Docker-Wrangler mode the long-lived connection holds a server thread and prevents connection reuse.

Remediation

Reduce `StreamRecoveryManager` timeout to under 25 seconds for Workers compatibility, or implement proper resumable streaming via a queue (e.g., push partial responses to KV or R2 and let the client poll/SSE reconnect). Consider a client-side reconnect protocol rather than server-side retry.

08

highDAT-001Data

LLM API keys stored in non-httpOnly, JS-readable cookies

All LLM provider API keys are serialised as JSON into the `apiKeys` browser cookie with no `httpOnly` or `SameSite` flag set by the application, making them readable by any script running on the page.

Impact

A single XSS vulnerability, malicious dependency, or browser extension exposes every configured API key. Keys appear in the Cookie request header for every same-origin request, increasing their surface in server access logs. The custom cookie parser in `cookies.ts` can also behave differently from the browser spec on malformed input, producing silent parse errors.

Remediation

Remove API keys from cookies entirely. Store them client-side in an origin-isolated store (localStorage or a dedicated encrypted object) and send them in a custom request header (e.g. `X-API-Keys`) rather than a cookie. If cookies must be used, set `httpOnly: true` and `SameSite=Strict`. Replace the hand-rolled parser with `URLSearchParams` or the `cookie` npm package.

09

highDAT-002Data

Settings export includes all API keys and OAuth tokens in plaintext

`ImportExportService.exportSettings()` serialises all cookies (including `apiKeys`) and all localStorage keys (including `supabase_connection`, `supabaseCredentials`, and `github_*` tokens) into a single downloadable JSON file.

Impact

A user who shares or loses the export file inadvertently exposes every LLM API key, Supabase management token, Supabase anon key, and GitHub OAuth token stored in the browser. The `_raw` block in the export is a verbatim dump of all cookies and all localStorage — no filtering is applied.

Remediation

Exclude secrets from the export by default. If users need to transfer API keys, provide a separate, clearly-labeled 'Export API keys' flow that warns about sensitivity. Strip the `_raw` block and any fields named `apiKeys`, `token`, or `*Credentials` from the standard export output. Consider encrypting the export file with a user-supplied passphrase before download.

10

highDAT-003Data

Supabase access token and credentials stored unencrypted in localStorage

The Supabase management-API bearer token (`token`), anon key, and project URL are persisted to `supabase_connection` and `supabaseCredentials` in localStorage without any encryption.

Impact

Any JavaScript (XSS, malicious extension, devtools script) can read these tokens with `localStorage.getItem('supabase_connection')`. The management token has broad permissions over the user's Supabase project including database modification. The anon key, once leaked, can be used against the user's Supabase project by any caller who knows the project URL.

Remediation

Do not persist the management-API bearer token beyond the current browser session (use sessionStorage or in-memory state only). The anon key can be stored in localStorage but should be treated as semi-public and scoped with RLS on the Supabase side. Warn users in the UI that connecting Supabase stores credentials locally.

11

highDAT-004Data

No server-side backup — browser data clear causes permanent data loss

All chat history, snapshots, settings, and credentials exist exclusively in the user's browser IndexedDB and localStorage. There is no server-side persistence, no automated export, and no recovery path.

Impact

Clearing browser storage, uninstalling the browser, switching devices, or a browser bug that corrupts IndexedDB results in total and permanent loss of all chat history and project snapshots. Users have no indication that their data is at risk until it is already gone. This is especially acute for the Docker self-hosted deployment where users may expect server-side durability.

Remediation

Add a persistent warning in the UI that all data is browser-local and encourage regular exports. Provide an auto-export option (e.g. periodic JSON download or optional cloud sync via a user-supplied storage endpoint). For the Docker target, consider an optional server-side persistence mode backed by a mounted volume or SQLite database so administrators can rely on server backups.

12

highSEC-003Security

VITE_-prefixed integration tokens baked into client-side JavaScript bundle

Five categories of third-party access tokens are configured with the VITE_ prefix in .env.example (VITE_GITHUB_ACCESS_TOKEN, VITE_GITLAB_ACCESS_TOKEN, VITE_VERCEL_ACCESS_TOKEN, VITE_NETLIFY_ACCESS_TOKEN, VITE_SUPABASE_ACCESS_TOKEN), causing Vite to inline them into the browser bundle at build time.

Impact

Any browser user can read these tokens from the page JavaScript source. GitHub and GitLab tokens grant write access to code repositories; Vercel and Netlify tokens grant deployment and site-management access; the Supabase access token grants management-API access to the user's Supabase project. A malicious actor can exfiltrate or abuse these without the operator noticing.

Remediation

Rename all integration tokens to non-VITE_ environment variable names (e.g., GITHUB_ACCESS_TOKEN, VERCEL_ACCESS_TOKEN). Read them only in server-side route handlers. Create thin server-side proxy routes for any client-initiated API calls that currently rely on these tokens in the browser.

13

highSEC-004Security

Open SSRF proxy accepts arbitrary HTTPS targets with no authentication or domain allowlist

The git proxy route extracts the target domain from the URL path and proxies any HTTPS request, with Access-Control-Allow-Origin: * and redirect: follow, requiring no authentication and enforcing no domain restrictions.

Impact

Any browser or third-party client can route arbitrary HTTPS traffic through the deployed server, including requests to cloud metadata endpoints, internal network services reachable by the container, and external APIs. redirect: follow allows SSRF via open redirects hosted on legitimate domains. The server's egress IP reputation can also be abused for spam or scraping.

Remediation

Add an allowlist of permitted target domains (github.com, gitlab.com, raw.githubusercontent.com). Require a session-authenticated request. Change to redirect: manual or validate redirect targets against the same allowlist.

14

highSEC-005Security

Rate limiting bypassed by spoofing X-Forwarded-For header

The getClientIP function trusts the X-Forwarded-For, X-Real-IP, and CF-Connecting-IP headers to identify the rate-limit key, with no verification that the request is actually coming through a trusted proxy.

Impact

In Docker deployment (no Cloudflare proxy in front), any caller can set X-Forwarded-For: 1.2.3.4 and bypass all rate limits. This enables unbounded calls to the LLM API endpoints, which consume costly third-party API quotas and can cause operator billing impact. The withSecurity wrapper that applies rate limiting is also not used on the most sensitive routes (api.chat.ts, api.mcp-update-config.ts).

Remediation

In non-Cloudflare deployments, derive the client IP from the actual TCP connection (request.socket.remoteAddress or equivalent) rather than trusting forwarded headers. Gate header trust on a configuration flag that is only enabled when the app is known to be behind a trusted reverse proxy. Apply the withSecurity wrapper consistently to all high-value routes.

15

highAIE-001AI Engineering

No agent context files — contributors and AI tools have no project guidance

The repository contains no CLAUDE.md, AGENTS.md, .cursor/rules, .cursorrules, or GitHub Copilot instructions file; any AI coding tool used by a contributor operates with zero project-specific context.

Impact

AI-assisted contributors will produce code inconsistent with the project's architectural constraints (three-target deployment, WebContainer limitations, .server/ isolation boundary, provider BaseProvider pattern) because those constraints are not written anywhere an agent can read. Generic AI suggestions will ignore the Cloudflare Workers timeout limits, the cookie-based API key model, and the existing PromptLibrary abstraction — leading to contradictory or unsafe contributions that pass lint but introduce silent regressions.

Remediation

Create a CLAUDE.md (or AGENTS.md) at the repo root that covers: the three deployment targets and their incompatible constraints, the .server/ boundary and why it exists, the PromptLibrary system and how to add or modify prompts, the MCP approval model, the BaseProvider pattern for adding providers, the fact that there are no tests for the LLM pipeline and what the contributor's testing responsibility is, and the cookie-based API key flow with its security caveats. Keep it under 500 lines and scoped to non-obvious invariants.

16

highAIE-002AI Engineering

System prompt changes have no behavioral validation gate

The repository ships four distinct system prompts (default/fine-tuned, original, optimized, discuss) in app/lib/common/prompts/. Any contributor can modify them; CI will pass as long as TypeScript compiles and ESLint passes.

Impact

A prompt regression — e.g. removing the boltArtifact format requirement, weakening the database safety rules, or inadvertently disabling the chain-of-thought instruction — will not be caught before it reaches users. Because the prompts control the AI's code-generation behavior, silent regressions can cause the product to generate unsafe SQL, skip migrations, or produce incomplete artifacts for all users of the affected prompt variant. There are no snapshot tests, no golden-file comparisons, and no eval harness.

Remediation

Introduce a prompt regression test suite using a mock LLM (or recorded fixture responses) that asserts: (1) the boltArtifact tag appears in code-generation responses, (2) the database safety constraints are present in the system prompt string, (3) the discuss prompt never outputs boltArtifact tags, and (4) the CONTINUE_PROMPT triggers continuation without repeating artifact tags. Enforce this suite in CI as a required check. Consider adding golden-file snapshot tests for the full compiled system prompt strings so that diffs are visible in PRs.

17

highAIE-003AI Engineering

No written repo tenets — project invariants exist only as tribal knowledge

PROJECT.md explicitly states that acceptance criteria are intentionally omitted and developers get 'maximum flexibility.' No document defines what correct behavior means for the LLM pipeline, what tradeoffs agents and contributors must preserve, or what is forbidden.

Impact

Without written tenets, contributors (human or AI-assisted) cannot reliably distinguish a valid optimization from a breaking change. Critical invariants — do not exceed Cloudflare Workers 30s CPU limit, do not log API keys, do not store credentials in non-httpOnly cookies, do not add MCP transport types without approval — are not documented. This makes every large contribution a judgment call, with regressions detectable only by experienced maintainers who happen to review the PR.

Remediation

Write a short tenets document (or expand CONTRIBUTING.md) that explicitly lists: deployment-target constraints and what each target can and cannot do, LLM pipeline invariants (prompt format, token limit handling, context buffer size limit of 5 files), security invariants (API keys must not appear in logs, locked files must be enforced, MCP tools require explicit user approval), and contributor expectations (manual testing against the Docker target at minimum, provider additions require a getDynamicModels implementation). Link this from README.md and CONTRIBUTING.md.

18

highINF-001Infrastructure

Production container runs as root with no USER directive

The Dockerfile's production stage (bolt-ai-production) has no USER instruction, causing all runtime processes to execute as UID 0 inside the container.

Impact

Any server-side code execution — including via the unauthenticated MCP stdio RCE (SEC-002) — runs with full root privileges inside the container. A container escape from a root process gives the attacker root on the Docker host. This amplifies the blast radius of every server-side vulnerability from 'container compromise' to 'host compromise'.

Remediation

Add a non-root user before the CMD directive: `RUN useradd -m -u 1001 bolt && chown -R bolt:bolt /app` followed by `USER bolt`. Verify that `/app/bindings.sh` and `/root/.config/.wrangler` paths are accessible under the new user, adjusting the mkdir and chmod steps accordingly.

19

highINF-002Infrastructure

Production Docker service has no restart policy — crashes require manual recovery

The app-prod service in docker-compose.yaml has no `restart` field, defaulting to `no`, so any container exit leaves the service permanently down.

Impact

A process crash, OOM kill, or intentional container exit (e.g., triggered via the unauthenticated RCE in SEC-002 or a malformed LLM response) leaves the self-hosted deployment unavailable until an operator manually restarts the container. For operators running bolt.diy as a persistent service, this is an undetected outage with no automatic recovery.

Remediation

Add `restart: unless-stopped` to the app-prod service in docker-compose.yaml. For operators using `docker run` directly, document the `--restart unless-stopped` flag in the README and .env.example setup instructions.

20

highOBS-001Observability

No error tracking — production exceptions are invisible

No error tracking SDK (Sentry, Bugsnag, Rollbar, or equivalent) is present in package.json or anywhere in the codebase. Unhandled server exceptions appear only in container stdout with no capture, grouping, release tagging, or owner notification.

Impact

Any production crash, unhandled promise rejection, or LLM provider failure goes unnoticed until a user manually reports it. There is no way to detect regressions after a release, no stack trace retention across restarts, and no owner notification when errors occur. With 31 unauthenticated API routes (per SEC audit) and a known RCE surface (SEC-002), the inability to detect unexpected server-side errors is especially risky.

Remediation

This is a Remix/Node.js app. Install Sentry (`@sentry/remix`) — it provides first-class Remix support with server-side and client-side capture, source maps, release tagging via VITE_APP_VERSION, and a generous free tier. Add `sentry.server.config.ts` and `sentry.client.config.ts`, instrument the Remix entry points, and configure a SENTRY_DSN environment variable. Pair it with an alert rule for new issues so at least the operator's email receives notifications.

21

highOBS-002Observability

No uptime monitoring or alerting — outages are only discoverable via user complaints

No external uptime monitor is configured for any deployment target. The /api/health endpoint always returns {status: 'healthy'} with no dependency checks, and the Docker HEALTHCHECK targets the root URL rather than this endpoint.

Impact

A container crash, OOM kill, Wrangler runtime failure, or network outage leaves the deployment silently down until a user reports it. Given that the production docker-compose service has no restart policy (INF-002), the deployment stays down indefinitely. With LLM streaming and 60-second deploy polling loops, process-level failures can go undetected for extended periods.

Remediation

Add Better Stack Uptime (free tier: 3 monitors, 3-minute check intervals, email/Slack alerts) to monitor the production URL and /api/health. Better Stack integrates uptime alerting with log ingestion, enabling incident correlation in one platform. Update the /api/health route to perform a shallow dependency check (verify Wrangler bindings are accessible) so it fails meaningfully. Fix the Docker HEALTHCHECK to target /api/health (see INF-006).

22

highOBS-003Observability

No structured searchable logs — production debugging relies on ephemeral container stdout

The server-side logger (app/utils/logger.ts) emits colorized plain-text lines to console. Logs contain no JSON structure, no requestId or correlationId, no environment tag, and no release tag. 505 raw console.log/warn/error calls across 131 files supplement this with unstructured ad hoc output.

Impact

Debugging a production incident requires live container access or examining transient stdout with no retention. It is impossible to search logs by route, user session, LLM provider, error type, or release. Correlating a user-reported failure to a specific request is infeasible. With sensitive workflows (git proxy, Supabase SQL proxy, LLM streaming, MCP stdio) generating server-side events, the absence of searchable structured logs means any multi-step failure investigation is blind.

Remediation

This is a Node.js app. Replace the colorized console logger with Pino (`pino`) — the fastest structured JSON logger for Node.js. Pino emits machine-parseable JSON natively, supports scopes/bindings for requestId, route, provider, and release fields, and has minimal overhead. Ship logs to Better Stack (Logtail) using `pino-logtail` for log search and 3-day free retention. Add a requestId to each incoming request using a Remix `loader` wrapper and propagate it through the logger context for all server-side calls.

23

highDEL-001Delivery

Docker publish runs concurrently with CI — untested code ships to GHCR :latest

docker.yaml and ci.yaml both trigger on push to main with no dependency between them, so the Docker image can be published to GHCR :latest before the Test job completes.

Impact

End-users and operators who pull ghcr.io/stackblitz-labs/bolt.diy:latest immediately after a main-branch push may receive an image built from code that has not yet passed type-check, lint, or unit tests. A broken merge that fails CI will still produce and distribute a broken Docker image. Because this is an open-source self-hosted tool, many operators rely on :latest as their update mechanism.

Remediation

Add a dependency in docker.yaml so the docker-build-publish job only runs after the CI test job succeeds on the same commit. The simplest approach is to use workflow_run: workflows: ['CI/CD'], types: [completed], branches: [main] as the trigger for docker.yaml, combined with a job-level condition: if: github.event.workflow_run.conclusion == 'success'. Alternatively, combine both jobs in a single workflow file with needs: [test] on the Docker job.

24

highDEL-002Delivery

No LLM regression, prompt, or eval tests in CI for an AI coding assistant

The CI pipeline has no prompt regression tests, eval datasets, guardrail checks, structured-output schema tests, or tool-call safety tests — despite LLM interactions being the core product feature.

Impact

Prompt changes, system prompt refactors, context-management changes, and provider configuration updates merge to main and ship to all self-hosted deployments with no automated verification of expected LLM behavior. Regressions in code-generation quality, safety guardrails, or tool-call handling are only discovered by users after deployment. For an LLM coding assistant that executes generated code in a WebContainer, guardrail regressions carry elevated risk.

Remediation

Add a vitest-based eval suite covering: (1) system prompt structure and critical instruction presence, (2) structured output schema compliance for common code-generation patterns, (3) MCP tool-call approve/reject guardrail behavior with representative adversarial prompts, and (4) at minimum a mocked LLM response test for the streaming API route. Wire this as a required CI gate. Consider a lightweight golden-dataset test that snapshots LLM context assembly (select-context.ts, create-summary.ts) against known inputs to catch accidental prompt truncation.

25

highDEL-003Delivery

Feature flag system is a non-functional stub — no kill switch for production changes

app/lib/api/features.ts returns hardcoded mock data with TODO comments; markFeatureViewed is a console.log stub. No real flag system controls rollout of any feature.

Impact

Every change ships to 100% of users on deploy with no ability to do gradual rollout, target specific user segments, or disable a broken feature without a full redeploy. For an AI coding assistant handling git operations, MCP server management, and Supabase SQL execution, a misbehaving feature has no kill switch short of a hotfix deploy. The stale mock entries (Dark Mode, Tab Management from 2024) also pollute any UI that consumes feature flag data.

Remediation

Replace the mock with a minimal environment-variable-driven flag system: read flag values from FEATURE_* env vars (or a JSON blob env var) at runtime, with clearly named flags, documented purpose, and expected removal timelines. For a self-hosted open-source app, a simple key-value store in the Wrangler bindings or a small JSON file is sufficient. Remove stale mock entries. Document the flag lifecycle policy in CONTRIBUTING.md.

26

highDOC-001Documentation

No architecture documentation for a system with three incompatible deployment targets

The repository has no architecture doc, system overview, or deployment-constraints guide, despite targeting Cloudflare Pages, Docker/Wrangler, and Electron simultaneously under a single codebase with fundamentally different runtime limits.

Impact

A new engineer or AI-assisted contributor cannot discover the 30-second Cloudflare Workers wall-clock limit, the mandatory .server/ isolation boundary, the WebContainers API licensing requirement, the cookie-based API key model, or the consequences of writing a blocking polling loop in an HTTP handler. Contributions that violate these invariants pass lint and typecheck, are reviewed without a written baseline, and ship with silent runtime failures in production (documented by ARC-001, ARC-003, INF-002, and others in this audit).

Remediation

Write an ARCHITECTURE.md (or equivalent docs/architecture.md) covering: the three deployment targets and their incompatible constraints (Workers CPU limits, Electron Node.js target, Docker Wrangler dev-server), the .server/ vs .client/ boundary and why it exists, the WebContainers API licensing boundary, the client-side-only persistence model, the PromptLibrary and BaseProvider extension points, and the MCP transport model. Cross-link from the README and CONTRIBUTING.md.

27

highDOC-002Documentation

No agent/AI context files — AI-assisted contributors operate with zero project guidance

No CLAUDE.md, AGENTS.md, .cursor/rules, or GitHub Copilot instructions file exists in the repository; AI coding tools used by contributors receive no project-specific context.

Impact

AI-assisted contributions will routinely ignore the Cloudflare Workers timeout limits, the .server/ isolation boundary, the BaseProvider extension pattern, the cookie-based API key security caveats, and the no-auth deployment model. These gaps are not surfaced by lint or typecheck, so unsafe or architecturally inconsistent code passes all CI gates and lands in the codebase unchallenged. This is confirmed by the ai-engineering audit (AIE-001).

Remediation

Create a CLAUDE.md (or AGENTS.md) at the repository root covering the non-obvious invariants: three deployment targets and their incompatible constraints, the .server/ boundary, the PromptLibrary system, the MCP approval model, the BaseProvider pattern, the no-auth design and its intended deployment scope, and the cookie-based API key flow with its security caveats. Keep it under 500 lines focused on gotchas, not re-documenting the README.

28

mediumCOD-003Code Quality

Duplicated fetch-and-map logic across 22 provider files

Each provider independently implements the same pattern: resolve API key, fetch models endpoint, filter response, and map to ModelInfo — including hardcoded model capability tables that are copy-pasted with minor per-provider differences.

Impact

Any logic change (timeout handling, error format, model filtering) must be replicated in up to 22 files. The BaseProvider already exists as an abstraction point but the getDynamicModels body pattern is not pulled into it. Model capability tables (contextWindow fallbacks, maxCompletionTokens per model ID prefix) are maintained separately in each file and will drift.

Remediation

Extract a shared fetchAndMapModels() helper on BaseProvider that accepts endpoint, headers, filter predicate, and mapper callback. Move model capability lookups (contextWindow, maxCompletionTokens) into a shared constants map keyed by model-name prefix patterns, referenced by all providers.

29

mediumCOD-004Code Quality

Quality CI gates all use `continue-on-error` and never block

The quality.yaml workflow runs dead-code detection (unimported), unused-dependency checks (depcheck), complexity analysis (es6-plato), and accessibility tests — all with `continue-on-error: true`, so none can ever fail a PR.

Impact

These checks provide the appearance of quality enforcement without the reality. Unused dependencies, dead code, and growing complexity accumulate silently because no CI gate rejects them. The only enforced gates are typecheck and lint in ci.yaml.

Remediation

Remove `continue-on-error: true` from at least the depcheck and unimported steps, or move them to a dedicated non-blocking informational workflow with clear documentation that they are advisory only. Add a vitest coverage threshold (e.g., `--coverage --coverage.thresholds.statements=50`) to the test step in ci.yaml.

30

mediumCOD-005Code Quality

Debug `console.log` statements left in server-side production code

Three server-side files contain bare console.log calls that bypass the structured logger: `constants.ts` logs a regex debug test, `stream-text.ts` logs a no-locked-files message, and `api.chat.ts` logs message counts.

Impact

These statements emit unstructured output to server logs, interfering with log parsing, potentially leaking message volume or internal state, and indicating unfinished debugging sessions were merged to main.

Remediation

Remove all bare `console.log` calls from server-side files. Replace with the existing `createScopedLogger` pattern already used throughout the codebase (e.g., `logger.debug()`). Add an ESLint rule (`no-console`) to prevent future occurrences in `app/lib/.server/` and `app/routes/`.

31

mediumARC-004Architecture

API keys transmitted and stored in browser cookies

LLM provider API keys are stored as `apiKeys` in a browser cookie and parsed from the `Cookie` header on every `api/chat` request.

Impact

Cookies are accessible to JavaScript (unless `httpOnly` is set), included in all same-origin requests (CSRF risk), and appear in server access logs. A single XSS vulnerability or log leak exposes all provider API keys. Keys are never rotated server-side; compromise is silent. Additionally, the cookie is parsed with a hand-rolled parser that may behave differently from the browser's cookie spec on edge cases.

Remediation

Store API keys in `localStorage` or a dedicated encrypted store client-side and send them in a custom request header (`X-API-Keys`) rather than a cookie. Apply `httpOnly` and `SameSite=Strict` to any cookies that must remain. Consider encrypting the key bundle with a session-derived key before storage.

32

mediumARC-005Architecture

Supabase SQL proxy accepts and forwards arbitrary queries without validation

The `/api/supabase/query` route proxies any `query` string received in the POST body to the Supabase Management API without schema validation, query allowlisting, or statement type restrictions.

Impact

If an attacker gains access to the Authorization token (which is passed through from the client), they can execute destructive SQL (DROP, TRUNCATE, DELETE) against the user's Supabase project through this server-side proxy. The proxy also surfaces Supabase error details including stack traces to the caller.

Remediation

Validate that the query body is a SELECT-only statement (parse with a lightweight SQL parser or simple regex). Do not surface Supabase internal error details to the client. Consider restricting this endpoint to read-only credentials if the integration only needs to display query results.

33

mediumARC-006Architecture

Three-target deployment creates incompatible runtime constraints in one codebase

The same Remix codebase targets Cloudflare Workers (via Wrangler), Docker (via Wrangler Pages dev), and Electron (via a custom Remix request handler), each with different timeout, API surface, and runtime module capabilities.

Impact

Runtime-specific bugs are hard to reproduce: code that works in Docker silently fails on Workers due to CPU limits, and Electron-specific paths (`@remix-run/node`) are bundled alongside Cloudflare-specific paths (`@remix-run/cloudflare`). The Electron entrypoint passes an empty `cloudflare: {}` object to the Remix handler, which masks any Worker-binding-dependent code. CI currently validates only the Docker target.

Remediation

Add a runtime capability matrix to CI: run smoke tests against a Wrangler miniflare environment to catch Worker-specific failures early. Consider extracting the Electron build to a separate config entry that explicitly opts in to Node.js-only APIs rather than sharing the Cloudflare build target.

34

mediumARC-007Architecture

MCP service singleton carries in-memory tool connection state across requests

MCPService uses a module-level singleton that holds live MCP client connections (stdio and SSE transport objects) and is mutated by `updateConfig` calls from the HTTP route.

Impact

In the Cloudflare Workers deployment model, singleton state does not survive between Worker invocations — each request may get a fresh MCP client with no connections. In Docker the singleton persists but is not protected by any lock: concurrent `updateConfig` calls can race and leave stale or partially-initialized client references. There is no connection health check or reconnect path.

Remediation

For Workers: accept that MCP state must be re-initialized per request or use Durable Objects for persistent connection state. For Docker: add a mutex or connection-state machine around `updateConfig` to prevent concurrent initialization races. Implement a health-check or ping before using any cached MCP client.

35

mediumDAT-005Data

Supabase query proxy leaks internal stack traces to callers

On a 500 error in `api.supabase.query.ts`, the response body includes `error.stack` from the caught JavaScript exception.

Impact

Stack traces expose server-side file paths, package versions, and internal error details to any client. This information aids targeted attacks against the server or the Supabase Management API integration.

Remediation

Remove the `stack` field from all error responses. Log the stack trace server-side via the logger, but return only a generic message and a stable error code to the caller.

36

mediumDAT-006Data

Cookie JSON parsing calls JSON.parse without try/catch

`getApiKeysFromCookie` calls `JSON.parse(cookies.apiKeys)` and `getProviderSettingsFromCookie` calls `JSON.parse(cookies.providers)` with no error handling.

Impact

A malformed or truncated `apiKeys` or `providers` cookie value causes an unhandled exception that propagates up through every server-side request handler that calls these functions, potentially crashing the request and returning a 500 to the user.

Remediation

Wrap both `JSON.parse` calls in try/catch blocks and return an empty object on parse failure. Consider using a validated parser (e.g. `JSON.parse` + Zod `safeParse`) so the returned value is always structurally correct.

37

mediumDAT-007Data

Non-atomic chat creation allows race conditions across concurrent tabs

`createChatFromMessages` calls `getNextId`, `getUrlId`, and `setMessages` as three independent IndexedDB transactions with no locking or atomicity guarantee.

Impact

If two browser tabs call `createChatFromMessages` concurrently (e.g. via import), both may receive the same numeric ID from `getNextId`, and the later write will silently overwrite the earlier one. The `urlId` uniqueness index will catch collisions at write time and throw, leaving one tab with an orphaned chat.

Remediation

Use a single `readwrite` IndexedDB transaction that covers both the key-generation read and the write. Alternatively, switch to `crypto.randomUUID()` for both `id` and `urlId`, which eliminates collision checks entirely and removes the O(n) scan.

38

mediumDAT-008Data

O(n) full scans for ID and URL-ID generation degrade with large histories

`getNextId` calls `store.getAllKeys()` and iterates to find the maximum numeric ID; `getUrlIds` opens a full cursor over all chats to collect existing URL IDs. Both are O(n) in the number of stored chats.

Impact

Users with large chat histories experience latency on every new chat creation and chat fork. IndexedDB cursor scans can block the main thread for tens of milliseconds on low-end or mobile devices, producing visible UI jank.

Remediation

Replace `getNextId` and `getUrlId` with `crypto.randomUUID()`. If sequential numeric IDs are required for UX, maintain a single `meta` record in a dedicated object store that holds a monotonic counter, updated atomically in the same transaction as the chat write.

39

mediumDAT-009Data

Settings import applies user-supplied data to localStorage and cookies without schema validation

`importSettings` and `_importComprehensiveFormat` iterate over keys in a user-supplied JSON file and write values directly to `localStorage` and cookies using `as any` casts throughout.

Impact

A maliciously crafted import file can overwrite arbitrary localStorage keys (including security-sensitive ones such as `supabase_connection` and `github_*` tokens) with attacker-controlled values. Legacy format import iterates every key in the file without any allowlist, widening the attack surface to any key the application reads from localStorage.

Remediation

Define an explicit allowlist of importable keys for both localStorage and cookies. Validate values with Zod schemas before writing. Reject import files that contain keys outside the allowlist or values that fail validation.

40

mediumSEC-006Security

Unauthenticated diagnostics endpoint exposes server environment details

GET /api/system/diagnostics is publicly accessible and returns the Node environment, which integration tokens are set, request headers, host URL, and external API reachability — all without authentication.

Impact

Attackers can use this endpoint for reconnaissance: confirming which API keys are configured (hasGithubToken, hasNetlifyToken), determining the server environment and host, and validating which external services are reachable. This directly aids targeting follow-on attacks.

Remediation

Remove this endpoint in production builds or restrict it to localhost. If needed for operational troubleshooting, gate it behind the same admin secret recommended for SEC-001.

41

mediumSEC-007Security

Content Security Policy uses unsafe-inline and unsafe-eval, neutralizing XSS protection

The CSP defined in createSecurityHeaders() includes script-src 'self' 'unsafe-inline' 'unsafe-eval', which allows any inline script and eval() to execute on the page.

Impact

An XSS vulnerability via LLM-generated markdown, the web-content fetcher, or a malicious MCP tool response would have full JavaScript execution capability, enabling session hijacking, API key theft from cookies and localStorage, and exfiltration of all user data. The CSP provides no meaningful XSS protection in its current state.

Remediation

Remove unsafe-eval. Replace unsafe-inline for scripts with per-request nonces or hashes for the specific inline scripts generated by Remix/React hydration. Engage the React/Remix team's recommended CSP approach for SSR applications. Adopt a Content-Security-Policy-Report-Only header first to identify legitimate usages without breaking production.

42

mediumSEC-008Security

Vulnerability scanning gates are non-blocking and findings are not surfaced to reviewers

pnpm audit runs with continue-on-error: true and CodeQL is configured with upload: false, so neither dependency vulnerabilities nor SAST findings block pull request merges or appear in the GitHub Security tab.

Impact

Known vulnerable dependencies and SAST findings accumulate silently. Reviewers have no visibility into security findings during code review. Exploitable CVEs in the npm supply chain can ship to production without any gate or triage step.

Remediation

Remove continue-on-error: true from the pnpm audit step. Add security-events: write permission to the workflow and set upload: true in CodeQL analyze to surface findings to GitHub's Security tab. Configure Dependabot for automatic dependency update PRs. Define severity SLAs (e.g., Critical/High block release within 7 days).

43

mediumSEC-009Security

SSRF protection in web content fetcher misses IPv6 private ranges and DNS rebinding

isAllowedUrl blocks known private IPv4 ranges and localhost but does not block IPv6 ULA (fc00::/7) or link-local (fe80::/10) addresses. The URL is validated once before fetch() is called, leaving the window open for DNS rebinding attacks.

Impact

An attacker who controls a domain can serve a public IPv6 address at validation time, then rebind the DNS to an internal IPv6 host before the TCP connection is established. Similarly, cloud infrastructure accessible via IPv6 can be reached by supplying valid IPv6 addresses. Redirects followed by the default fetch() behavior can pivot to blocked addresses via a public redirect endpoint.

Remediation

Add IPv6 private range patterns to PRIVATE_IP_PATTERNS. Use redirect: 'manual' in the fetch call and re-validate redirect targets through isAllowedUrl. For full SSRF protection, perform a DNS pre-resolution of the target hostname and verify the resolved IP against the blocklist before fetching.

44

mediumSEC-010Security

Supabase credentials injected into system prompt from untrusted client request body

The chat action reads a supabase field from the client-supplied JSON request body and injects its anonKey and supabaseUrl values directly into the LLM system prompt without validation.

Impact

A malicious caller can supply a crafted supabase.credentials object containing prompt injection payloads, overriding the operator system prompt. Because the credentials are sourced from the client, they cannot be trusted as the user's actual Supabase project. An attacker can also substitute a foreign Supabase project URL, causing the LLM to advise queries against an attacker-controlled database.

Remediation

Source Supabase credentials from the server-side session or from a cookie that is validated server-side, not from the chat request body. If client-supplied credentials are necessary, validate that anonKey matches the expected format and that supabaseUrl matches the user's configured project URL stored server-side.

45

mediumSEC-011Security

MCP server configuration logged verbatim, including credentials and auth headers

MCPService.updateConfig logs the full MCPConfig object as JSON at debug level, including the env and headers fields of SSE and streamable-HTTP server configs that may contain bearer tokens or API keys.

Impact

MCP server credentials (auth headers, environment secrets) appear in plaintext server logs. In a shared logging infrastructure or if logs are shipped to a third-party collector, these credentials are exposed beyond the intended trust boundary.

Remediation

Before logging, redact env and headers fields from each server config. Create a sanitizeMCPConfig helper that replaces credential values with [REDACTED] while preserving structure.

46

mediumAIE-004AI Engineering

Three overlapping system prompts with no version or selection guidance

PromptLibrary exposes three prompt variants ('default', 'original', 'optimized') with overlapping content and no documented criteria for when each should be used, tested, or deprecated.

Impact

Contributors adding features to the system prompt must decide which of the three variants to update — a decision with no guidance. In practice, updates are likely applied to only one variant, causing behavioral divergence between prompt selections. The 'optimized' prompt is labeled 'experimental' but has no mechanism to graduate it to stable or retire it. Users who select a non-default prompt receive a different and potentially inconsistent experience.

Remediation

Document each prompt variant's intent, status (stable/experimental/deprecated), and maintenance owner in PromptLibrary's comments or in a companion prompts/README.md. Establish a policy for feature propagation across variants (e.g. security and database rules must be applied to all variants; cosmetic changes may differ). Add a CI check that verifies all variants contain the mandatory safety and format instructions.

47

mediumAIE-005AI Engineering

MCP stdio transport allows arbitrary command execution with no contributor vetting guidance

MCPService supports a stdio transport that spawns system processes defined by end-user configuration; no contributor documentation describes what review is required before shipping changes to this transport or adding new ones.

Impact

A contributor adding a new MCP transport type or modifying the stdio command execution path has no documented security review checklist. The current user-facing approval UX (approve/reject per tool call) is a runtime control, but it does not protect against a contributor accidentally weakening the command sandboxing or adding a transport that bypasses the approval flow. In the self-hosted Docker deployment, stdio MCP servers run with the container's process privileges.

Remediation

Add a section to CONTRIBUTING.md (or the tenets document) that defines: what security review is required before shipping changes to MCP transport code, what the approve/reject flow must always preserve, and how to test that new transport types do not bypass the tool execution approval gate. Consider adding an integration test that verifies TOOL_EXECUTION_DENIED is returned when a user rejects a tool call, regardless of transport type.

48

mediumAIE-006AI Engineering

Context selection and summarization sub-agents use same user-selected model with no quality guard

selectContext() and createSummary() both use the same model the user selected for chat, which may be a small or specialized model (e.g. a code-only model) not suited for summarization or XML-tag structured output.

Impact

If the user selects a model with weak instruction-following (e.g. a fine-tuned code model from an openai-like endpoint), the summarization and context-selection sub-agents may produce malformed XML responses. selectContext() already throws 'Invalid response. Please follow the response format' and 'Bolt failed to select files' on parse failure — these errors surface as broken UX mid-conversation. There is no fallback to a known-good model for these utility calls.

Remediation

Either (a) hardcode a reliable model (e.g. the provider's default model or a designated fallback) for summarization and context-selection calls regardless of user selection, or (b) add output validation with a retry on parse failure before surfacing an error. Document this design constraint so contributors modifying createSummary() or selectContext() understand why model selection is handled differently here.

49

mediumINF-003Infrastructure

Production runtime is wrangler pages dev — a development-mode server

The `dockerstart` npm script executes `wrangler pages dev ./build/client $bindings --ip 0.0.0.0 --port 5173 --no-show-interactive-dev-session`, running Wrangler's local development server as the production process.

Impact

Wrangler Pages dev is documented and designed as a local development tool. Its production stability characteristics, memory management, connection handling, graceful shutdown behavior, and performance under concurrent load are undefined and untested. Cloudflare explicitly distinguishes this from the actual Pages edge runtime. Production deployments inherit undocumented limits and behaviors that may differ from both the Cloudflare edge and a standard Node.js server.

Remediation

Evaluate migrating Docker deployments to use `@remix-run/serve` or a lightweight Node.js HTTP adapter (e.g., the existing `@remix-run/node` runtime already in the dependency tree) rather than Wrangler. If the Wrangler runtime is required to simulate CF Workers bindings, document this explicitly as a known limitation and add a CI smoke test that runs the full container under simulated load to characterize its behavior.

50

mediumINF-004Infrastructure

No container image vulnerability scanning in the Docker publish pipeline

The docker.yaml publish workflow builds and pushes to GHCR without running any vulnerability scanner against the resulting image.

Impact

OS-level CVEs in node:22-bookworm-slim (Debian Bookworm), installed apt packages (git, curl), and production npm dependencies that manifest as native binaries are never surfaced. Consumers pulling `ghcr.io/stackblitz-labs/bolt.diy:latest` receive an image that has not been scanned since the last manual check, with no evidence trail of scan results for compliance purposes.

Remediation

Add an Aqua Security Trivy or Docker Scout scan step immediately after the `docker/build-push-action` step in docker.yaml. Set `--exit-code 1 --severity CRITICAL` to fail the workflow on critical OS-level CVEs. Upload the SARIF results to GitHub Security with `--format sarif` and `upload-artifact` to create an audit trail.

51

mediumINF-005Infrastructure

No staging environment for server-side changes before GHCR image publication

The preview.yaml workflow deploys only `build/client` (the static client bundle) to Cloudflare Pages, leaving all server-side Wrangler/API changes untested against a running container prior to image publication.

Impact

Server-side changes to API routes, the LLM streaming layer, MCP service, git proxy, and Supabase SQL proxy are merged to main and published to GHCR:latest without being validated against a live Docker environment. Regressions in server-side behavior are only discovered by end-users who pull and run the image.

Remediation

Add a Docker integration test job to ci.yaml that builds and starts the production container, waits for the HEALTHCHECK to pass, and runs a suite of smoke tests against the running container (at minimum: HTTP 200 on /api/health, correct CORS headers on /api/git-proxy, and a basic chat API round-trip). Alternatively, promote images from a validated staging tag (e.g., `:staging`) to `:latest` only after integration tests pass.

52

mediumOBS-004Observability

LLM prompt and response content persisted in browser cookies and exportable in plaintext

The logStore writes LLM provider logs including `details.prompt` and `details.response` fields to the `eventLogs` cookie. EventLogsTab.tsx renders and exports these as JSON, CSV, PDF, and TXT files without any redaction.

Impact

Full LLM conversation context — including system prompts, user instructions, generated code, and Supabase credentials injected into the system prompt (see SEC-008 in the AI-engineering audit) — is serialized into a browser cookie. Cookies are readable by any JavaScript running on the page (noting cookies in this app are not httpOnly per SEC-001 context), are included in HTTP requests to the server, and are exportable to disk. If the exported debug bundle is shared in a GitHub issue or support channel, sensitive prompt content is disclosed.

Remediation

Redact or omit `prompt` and `response` fields from logStore provider entries before persisting to cookies. Store only metadata (model, provider, tokenCount, duration, error) in the cookie-backed log store. Keep full prompt/response visible in the in-memory EventLogsTab display if needed for debugging, but strip them from the exported bundle or add a clear 'contains sensitive content' warning before export. Move the log persistence layer off cookies to sessionStorage or IndexedDB to avoid including log data in HTTP requests.

53

mediumOBS-005Observability

Diagnostics endpoint exposes operational topology unauthenticated

GET /api/system/diagnostics requires no authentication and returns NODE_ENV, boolean presence of GitHub/Netlify tokens, cookie presence, active API endpoint paths, GitHub API connectivity status, Netlify API connectivity status, User-Agent, Host header, request URL, and CORS configuration.

Impact

An attacker can probe the deployment to confirm which integrations are active, whether tokens are configured, which API endpoints are reachable, and what runtime the server is running — without any credentials. This operational intelligence reduces the cost of targeted attacks and confirms the deployment's configuration surface. In combination with the unauthenticated key export endpoint (SEC-001), this creates a detailed reconnaissance capability.

Remediation

Restrict /api/system/diagnostics behind the same admin secret gate recommended for SEC-001 and SEC-002. For developer debugging purposes, the diagnostic information can remain available in the in-browser settings panel (where the user is already present) without exposing it as an unauthenticated HTTP endpoint. If the endpoint must remain public, strip the environment variable presence flags and active token indicators.

54

mediumOBS-006Observability

No LLM spend visibility or provider error alerting

Token usage is tracked in a per-session client-side variable (cumulativeUsage in api.chat.ts) and logged to the in-browser logStore only. There is no persistent token/cost tracking, no spend alert, and no visibility into LLM provider error rates or latency trends.

Impact

The app proxies LLM calls for all configured providers using operator-supplied API keys. A runaway loop, model misconfiguration, or compromised key (easily stolen via SEC-001) can drive unbounded API spend with no alert to the operator. LLM provider outages or degraded latency are undetectable — the only signal is a user seeing slow or failed responses. With 13+ provider integrations supported, provider-specific failure modes are invisible.

Remediation

Log token usage and LLM call latency to a persistent destination (Better Stack or the structured log pipeline recommended in OBS-003). Configure a spend alert on each LLM provider's console (all major providers support usage alerts). For multi-operator deployments, consider integrating a lightweight LLM observability layer such as Langfuse (self-hostable, open-source) which captures model, provider, token usage, latency, and error rate per request with minimal setup.

55

mediumDEL-004Delivery

Required CI check validation step is itself advisory — PRs can merge despite failing checks

The quality-gates job in pr-release-validation.yaml uses continue-on-error: true on the check-required-status-checks step, meaning a failed required check does not block the workflow or the PR.

Impact

The pr-release-validation workflow is intended to enforce that 'Test' and 'CodeQL Analysis' pass before merge. With continue-on-error: true, the workflow always reports success regardless of check status. Maintainers may assume the gate is active when it is not, leading to merges with failing tests or SAST findings going undetected.

Remediation

Remove continue-on-error: true from the check-required-status-checks step. Additionally, configure GitHub branch protection rules on main to require the 'Test' status check to pass before merge — this is the only reliable enforcement mechanism, as workflow-level checks cannot prevent merges without branch protection backing them.

56

mediumDEL-005Delivery

Release triggered by magic commit-message string with force-push to stable branch

update-stable.yml fires when a commit to main contains '#release', then force-pushes the stable branch with --force, losing the branch's prior commit history.

Impact

Any contributor with merge rights can trigger a production release by including '#release' in any commit message — there is no separate release review, approval step, or release PR. The force-push to stable means prior release history is not retained on the branch; rolling back to a previous stable state requires finding the correct git tag rather than reverting the branch. The workflow also runs git pull before the commit-and-push, creating a window for a race condition if two release commits land close together.

Remediation

Replace the magic-string trigger with a dedicated release workflow that requires a manual workflow_dispatch trigger with explicit version and change inputs, or that is gated on a PR labeled 'release' after approval. Change the stable branch update from --force to a standard merge or rebase that preserves history. Document the release procedure in CONTRIBUTING.md.

57

mediumDEL-006Delivery

Security workflow uses unpinned aquasecurity/trivy-action@master — supply chain risk

The secrets-scan job in security.yaml references aquasecurity/trivy-action@master, which resolves to the latest HEAD of the master branch at execution time.

Impact

A compromised or malicious push to the trivy-action master branch would execute arbitrary code in every subsequent CI run with access to GITHUB_TOKEN and all CI secrets. Since this action runs in the security scanning workflow that executes on push to main and on PRs, the blast radius includes all workflow secrets and the ability to push artifacts. Using a mutable branch reference defeats the supply-chain protection that pinned actions provide.

Remediation

Pin aquasecurity/trivy-action to a specific release SHA: replace @master with the full SHA of a known release (e.g. @a20cc5943ec6e849e10b0c53a3c5da04a9e2e07d for v0.28.0). Apply the same pinning discipline to all other actions currently using tag aliases (actions/checkout@v4, docker/setup-buildx-action@v3, etc.) by replacing with their corresponding commit SHAs.

58

mediumDEL-007Delivery

Shared setup-and-build action installs dependencies without --frozen-lockfile

The .github/actions/setup-and-build/action.yaml composite action runs pnpm install without --frozen-lockfile, allowing the lockfile to be updated during CI runs.

Impact

If the pnpm lockfile is out of sync with package.json (e.g. after a dependency version bump that wasn't followed by a lockfile commit), pnpm install will silently update the lockfile in CI rather than failing. This means CI may be testing a different dependency tree than what developers tested locally, and what ends up in the Docker image differs from what the lockfile says. This action is used in ci.yaml, quality.yaml, preview.yaml, and test-workflows.yaml.

Remediation

Add --frozen-lockfile to the pnpm install command in .github/actions/setup-and-build/action.yaml: run: pnpm install --frozen-lockfile && pnpm run build. This matches the practice already used in security.yaml and the Docker build stage, and will cause CI to fail with a clear error if package.json and pnpm-lock.yaml are out of sync.

59

mediumDEL-008Delivery

Electron build workflow publishes releases with no quality gates

electron.yml proceeds directly from checkout to electron:build:* to Create Release with no type-check, lint, or test steps.

Impact

Electron releases for Windows, macOS, and Linux are distributed to end-users who download and install native applications from GitHub Releases. A broken build triggered by a tag push or workflow_dispatch will produce and publish installers for all three platforms with no automated quality checks. Because Electron users receive updates through the native download flow rather than pulling a Docker image, they have no equivalent of a SHA-tagged image to roll back to.

Remediation

Add setup-and-build, pnpm run typecheck, and pnpm run test steps before the Build Electron app step in electron.yml. Consider reusing the .github/actions/setup-and-build composite action for consistency. For releases triggered by tag pushes, require the CI/CD workflow to have passed on the same tag commit before the Electron release is published.

60

mediumDOC-003Documentation

Duplicate FAQ and CONTRIBUTING files have diverged with contradictory content

FAQ.md and CONTRIBUTING.md exist both at the repo root and under docs/docs/, and the two FAQ copies now recommend different AI models — the root version suggests 'Claude 3.5 Sonnet (old)' while the docs site version lists Claude 4 Opus and xAI Grok 4.

Impact

Contributors and users who read the root FAQ get outdated model recommendations that affect prompt quality and cost. The two-track maintenance pattern means either copy can fall further behind with no mechanism to detect drift. The same risk applies to CONTRIBUTING.md if the two versions diverge on PR process or coding standards.

Remediation

Designate one canonical location (root or docs/docs/) and delete the duplicate. If the MkDocs site is the intended canonical source, the root files should be stubs that redirect to the docs site, or the GitHub Actions docs.yaml workflow should auto-sync them. Add a CI check that fails if the root and docs/docs/ versions of the same file diverge beyond a known-acceptable delta.

61

mediumDOC-004Documentation

No operational runbooks — production operators have no incident or recovery guidance

No deployment playbook, rollback procedure, incident response guide, secret rotation doc, or crash-recovery procedure exists in any documentation file.

Impact

When a self-hosted container crashes (no restart policy per INF-002), an operator has no documented procedure for diagnosing the failure, recovering service, or checking for security compromise. The same gap applies to secret rotation (all API keys in .env.local), Wrangler misconfiguration, Docker volume corruption, and the LLM provider outage scenario. This information gap amplifies the blast radius of every infrastructure and security finding in this audit.

Remediation

Add a minimal RUNBOOK.md (or docs/runbook.md) covering: starting and stopping the Docker service, checking health and logs, restarting after a crash, rotating API keys (update .env.local and restart), what to do when an LLM provider goes down, and how to back up and restore the application state. Link from the Docker deployment section in the README.

62

mediumDOC-005Documentation

No decision records for critical architectural, security, and provider choices

Decisions with significant ongoing consequences — no-auth design, cookie-based API key storage, three-target deployment model, use of wrangler pages dev as the production runtime, and the open git-proxy design — have no written rationale, tradeoff notes, or current-status record anywhere in the repository.

Impact

Future contributors cannot distinguish intentional constraints from overlooked gaps, cannot assess whether a decision still applies, and cannot safely revisit a design without replicating the original reasoning from scratch. This creates a recurring risk of contributors adding authentication for one route, expecting it to cascade, and shipping broken partial auth because the no-auth design was never formally scoped.

Remediation

Add at minimum an informal DECISIONS.md (or docs/decisions/) with one entry per major decision covering: the choice made, the alternatives considered, the rationale, and whether the decision is still current. Priority decisions to document: (1) no-auth posture and intended deployment scope, (2) cookie-based API key storage, (3) three incompatible deployment targets, (4) wrangler pages dev as production runtime, (5) open git proxy design.

63

mediumDOC-006Documentation

No user-facing legal policies for an app handling third-party credentials and user data

bolt.diy stores and proxies LLM API keys, GitHub/GitLab OAuth tokens, Vercel/Netlify access tokens, and Supabase credentials with no discoverable privacy policy or terms of service in the repository, documentation, or source routes.

Impact

Users self-hosting or using bolt.diy in a networked environment have no documented statement of what data is collected, how credentials are stored, and what the provider's obligations are. Any commercial or multi-user deployment of bolt.diy requires legal compliance with data handling regulations (GDPR, CCPA) that cannot be met without a published privacy policy. The WebContainers API licensing note in the README addresses commercial API licensing but not user-facing legal obligations.

Remediation

Add a PRIVACY.md and TERMS.md (or docs/legal/) describing data handling, credential storage, and scope of service. At minimum the privacy notice should cover: what data is stored client-side (IndexedDB, cookies), what credentials are transmitted server-side, and that the app does not store user data on any central server in the default self-hosted configuration. Link both from the README and the app's settings or help panel.

64

lowCOD-006Code Quality

Inconsistent error throwing pattern across provider implementations

Provider getDynamicModels() methods throw bare template literals (`throw `Missing Api Key...``) while getModelInstance() throws `new Error(...)`, creating two different error types from the same logical condition.

Impact

Callers using `error instanceof Error` or `error.message` will behave differently depending on which code path is exercised. The difference is subtle and only shows up at runtime when error metadata is inspected.

Remediation

Standardize all provider error throws to `throw new Error(...)`. Update the getDynamicModels pattern across all providers to use `throw new Error('Missing API key...')`. A simple ESLint rule or a codemod can catch/fix the bare string throws.

65

lowARC-008Architecture

URL ID generation performs a full sequential scan of all chat keys

`getNextId` calls `store.getAllKeys()` and iterates to find the highest numeric ID; `getUrlId` calls `getUrlIds` which opens a full cursor scan over all chats to collect existing URL IDs.

Impact

Both operations are O(n) in the number of chats. Users with large chat histories (hundreds or thousands of chats) will experience noticeable latency when creating new chats or forking existing ones. IndexedDB cursor scans can block the main thread for tens of milliseconds on mobile devices.

Remediation

Use a UUID or `crypto.randomUUID()` for both internal IDs and URL IDs to avoid collision checks. If sequential IDs are required for UX reasons, maintain a dedicated `meta` object store with a monotonic counter rather than scanning all keys.

66

lowARC-009Architecture

No server-side API versioning or contract documentation for internal routes

All 31 API routes are unversioned (`/api/chat`, `/api/netlify-deploy`, etc.) with request and response shapes documented only in TypeScript interfaces scattered across the client-side callers.

Impact

Breaking changes to route request/response shapes cannot be deployed gradually — all clients (web, Electron, any community integrations) must update atomically. The lack of versioning also makes it impossible to detect or test backward compatibility. Several request bodies are typed as `any` (e.g., `api.supabase.query.ts`, `api.mcp-update-config.ts`), removing compile-time safety.

Remediation

Introduce a `/api/v1/` prefix and add Zod schemas for all route request bodies validated at the handler boundary. Export request/response type definitions from a shared `types/api.ts` for use by both server and client callers.

67

lowDAT-010Data

Stale `snapshot:*` localStorage keys may persist after IndexedDB migration

Snapshots were migrated from localStorage (`snapshot:${id}` keys) to IndexedDB in version 2, but no cleanup pass removes the old keys. The `resetAllSettings` function still scans for and removes `snapshot:*` keys from localStorage, confirming they can persist.

Impact

Stale snapshot entries accumulate in localStorage indefinitely, consuming storage quota. On mobile or quota-constrained environments this can eventually trigger `QuotaExceededError` on unrelated localStorage writes.

Remediation

Run a one-time migration in `openDatabase` `onupgradeneeded` (version 3) that iterates localStorage, reads any `snapshot:*` key, checks if the corresponding IndexedDB snapshot already exists, and removes the localStorage key. Alternatively, add a startup cleanup task guarded by a `snapshotMigrationCompleted` flag in localStorage.

68

lowDAT-011Data

lockedFiles debounce can silently lose state on abrupt browser close

`saveLockedItems` applies a 300 ms debounce before writing to localStorage. If the browser or tab closes within that window, the in-memory changes are never persisted.

Impact

File and folder locks created in the last 300 ms before an unplanned close are silently lost. Users may find that previously locked files are editable by the AI on the next session, leading to unexpected modifications.

Remediation

Add a `beforeunload` event listener that calls `localStorage.setItem` synchronously (bypassing the debounce timer) if there are pending unsaved changes. Alternatively, remove the debounce for the `lockedFiles` key, as lock operations are infrequent and the performance benefit is minimal.

69

lowSEC-012Security

No pre-commit secret scanning hook installed for developers

The .husky/pre-commit hook runs lint-staged with type-checking and linting but no secret-detection scanner. The security.yaml Trivy scan only runs in CI after a push.

Impact

A developer can accidentally commit an API key or token that reaches the repository before Trivy detects it. Secrets committed to git history require key rotation even after removal, and in a public open-source repo the exposure window may be indexed before deletion.

Remediation

Add a pre-commit hook using gitleaks, detect-secrets, or truffleHog to scan staged files. Document the hook installation in CONTRIBUTING.md. Consider adding a GitHub Actions check that fails on any commit that introduces new secrets, in addition to the full-history Trivy scan.

70

lowAIE-007AI Engineering

No delivery documentation template for LLM pipeline changes

PR descriptions for prompt or provider changes have no required template capturing behavioral test evidence, model compatibility, known limitations, or rollback notes.

Impact

When a system prompt or provider change ships and causes a regression, maintainers cannot determine from the PR history what was tested, which models were verified, or how to roll back to the prior behavior. The squash-commit changelog captures what changed but not how it was validated or what risk it carries.

Remediation

Add a PR template (.github/pull_request_template.md) with a section for LLM-relevant changes that prompts contributors to describe: which models the change was tested with, what behavioral assertions were checked manually, any known limitations or edge cases, and whether the change affects all prompt variants or only one. This template can be optional for non-LLM changes but serves as a checklist for contributors working on prompts, providers, or the MCP service.

71

lowINF-006Infrastructure

Docker HEALTHCHECK targets the app root instead of the dedicated health endpoint

The Dockerfile HEALTHCHECK uses `curl -fsS http://localhost:5173/` rather than `http://localhost:5173/api/health`, which returns the full Remix HTML page.

Impact

A successful healthcheck response confirms the HTTP server is accepting connections but does not verify that the API layer, Wrangler bindings, or the application runtime are functional. A partially broken deployment (e.g., Wrangler runtime initialized but API routes returning 500) will pass the healthcheck and remain in service.

Remediation

Update the HEALTHCHECK CMD to `curl -fsS http://localhost:5173/api/health || exit 1`. The existing /api/health route already returns `{"status": "healthy"}` with a 200 status code.

72

lowINF-007Infrastructure

bindings.sh is vulnerable to shell word splitting on env var values with spaces or metacharacters

The bindings.sh script assembles a `$bindings` string by concatenating `--binding NAME=VALUE` entries and passes it unquoted to `wrangler pages dev`, relying on word splitting to separate arguments.

Impact

An env var value containing spaces, quotes, or shell metacharacters (e.g., a JSON-formatted AWS_BEDROCK_CONFIG value with spaces) will split incorrectly when the unquoted `$bindings` variable is expanded, potentially mangling the wrangler invocation or causing argument injection. The AWS_BEDROCK_CONFIG variable is explicitly documented as a JSON string, making this a realistic trigger.

Remediation

Replace string concatenation with a bash array: `bindings_array=(); bindings_array+=(--binding "${name}=${value}")` for each entry, then pass as `wrangler pages dev ... "${bindings_array[@]}"`. This safely handles values with spaces, quotes, and shell metacharacters.

73

lowOBS-007Observability

No incident runbook or operational reference for self-hosted deployments

No runbook, where-to-look guide, or operator incident reference exists anywhere in the repository. The README covers setup but not diagnosis, recovery, or known failure modes.

Impact

Operators who deploy bolt.diy and encounter a failure (container not starting, LLM calls failing, git proxy errors, Supabase connection issues) have no documented triage path. Given the complexity of the deployment (three incompatible runtime targets, 13+ LLM provider integrations, MCP server management), absence of operational documentation increases mean time to recovery for self-hosted deployments.

Remediation

Add a short `docs/operations.md` covering: (1) how to verify the deployment is healthy (/api/health, /api/system/diagnostics), (2) where to find container logs (docker logs, wrangler output), (3) common failure modes and their symptoms (expired API key, Wrangler binding failure, container OOM), (4) how to restart the service, and (5) how to download a debug log from the EventLogsTab. This requires minimal effort and directly reduces operator friction.

74

lowDEL-009Delivery

No Dependabot configured — dependency updates rely entirely on manual PRs

No .github/dependabot.yml is present, so dependency version updates across npm packages and GitHub Actions must be opened manually.

Impact

Security patches and compatibility updates for the project's 60+ direct npm dependencies and 15+ GitHub Actions references do not receive automated PRs. Given that pnpm audit runs with continue-on-error (non-blocking), vulnerable dependencies can accumulate without surfacing through any automated channel. Action references are mostly tag-pinned rather than SHA-pinned, so the combination of no Dependabot and no SHA-pinning means stale action versions are silently inherited.

Remediation

Add a .github/dependabot.yml that configures weekly updates for package-ecosystem: npm and package-ecosystem: github-actions, targeting the main branch. Pair this with removing continue-on-error from pnpm audit in security.yaml (or setting a high-severity threshold) so that Dependabot-opened PRs that don't fix high-severity CVEs can be identified and triaged.

75

lowDOC-007Documentation

CHANGES.md contains a feature design doc, not a changelog

CHANGES.md contains a detailed feature implementation narrative for the file-locking system, while changelog.md is the actual PR-based release changelog. The file names are swapped relative to convention.

Impact

Contributors looking for recent changes open CHANGES.md and find a single feature's implementation notes. The actual changelog in changelog.md is not prominently linked from the README. This creates confusion about where to find release history and what changed between versions.

Remediation

Rename CHANGES.md to something accurate (e.g., docs/features/file-locking.md or delete it if the PR description already captures this content) and ensure changelog.md is linked from the README's table of contents as the canonical release changelog.

76

lowDOC-008Documentation

docs/docs/index.md references a setup script that does not exist in the repository

The Docker setup section instructs users to run `./scripts/setup-env.sh` to sync .env.local and .env, but no such file or scripts/ directory exists in the repository.

Impact

Users following the Docker setup docs from the MkDocs site encounter a missing script error and must manually copy environment files, which is undocumented as the fallback. This creates silent setup failures that are difficult to diagnose for non-technical users.

Remediation

Either create the scripts/setup-env.sh helper script (a simple `cp .env.local .env`) and add it to the repository, or remove the reference from docs/docs/index.md and document only the manual `cp .env.local .env` step.