Purpose: This file tells Claude Code sessions what is stable, what can be changed, and how to behave in this repo.
Read this entire file at the start of EVERY session before making any change.
The following files/regions are locked. They contain working code that the user has tested and verified in production. Do NOT modify them even if you think there's a better way, a cleanup opportunity, or a bug fix needed in passing.
If you genuinely believe one of these files needs to change, STOP and ask the user explicitly. Do not modify as a side effect of another task.
apps/web/next.config.mjs
packages/caddy/src/config.ts ← route/handler builder — locked after Bug #1/#4 fix (dockee01)
packages/caddy/src/resolve-upstream.ts ← static upstream resolver (Bug #2 fix)
packages/caddy/src/regenerate-routes.ts ← routes array regeneration (Bug #6 fix)
packages/db/src/migrations/*.sql ← all existing migrations — add new migrations, never edit past ones
scripts/check-no-js-shadows.sh ← shadow guard — do not weaken
.gitignore (packages/*/src/**/*.js entries) ← shadow guard — do not remove
packages/api/src/routers/users.ts ← login/logout __setCookie mechanism (see Auth Cookie Handling below)
packages/api/src/trpc.ts ← createContext with resHeaders fallback (see Auth Cookie Handling below)
apps/web/src/app/api/trpc/[trpc]/route.ts ← tRPC response interceptor for Set-Cookie injection
docker-compose.yml ← /var/run/docker.sock mount required for network discovery
packages/caddy/src/verify.ts ← drift detection — do not simplify normalization rules (Fix 4)
packages/db/src/migrations.ts (Fix 4 entries) ← sync_status/sync_diff/sync_checked_at/sync_source — never edit existing DDL entries, only append
(Adjust these paths to match actual repo structure when you first populate this file.)
Never edit an existing migration file, even if you spot an error. If the schema needs to change:
- Create a new migration with incremented timestamp/number
- Write corrective SQL in the new migration
- Do NOT modify past migrations — other environments may have already applied them
packages/auth/src/**/*
packages/api/src/trpc/context.ts
apps/web/src/middleware.ts
Auth bugs that users report must be fixed through new code, not by reinterpreting existing session/CSRF logic. Ask before touching.
The following three files implement a workaround for a Next.js 15 standalone build bug where tRPC v11's resHeaders argument is not reliably plumbed through to procedure context. At the source level the destructuring looks correct, but the compiled standalone output produces Cannot read properties of undefined (reading 'append') crashes on any procedure that calls ctx.resHeaders.append().
The workaround is a three-file pattern. Do not change any of these files without explicit user approval.
-
packages/api/src/routers/users.ts— Theloginandlogoutprocedures each return a__setCookie: stringfield in their response body, alongside normal response data. Inside each procedure there's ALSO a best-effortctx.resHeaders.append()call guarded bytypeof ctx.resHeaders.append === 'function'— this works in dev, no-ops in standalone. Do NOT remove the__setCookiefield. Do NOT remove the defensive guard. Do NOT addnext/headersimports to this file (it breaks the build —packages/apiis not allowed to depend onnext). -
packages/api/src/trpc.ts—createContextacceptsopts: { req: Request; resHeaders?: Headers }(NOT destructured parameters) and initializesconst resHeaders = opts.resHeaders ?? new Headers()as a fallback. This prevents runtime crashes when Next.js standalone fails to passresHeaders. Do NOT revert this to destructured{ req, resHeaders }. Do NOT remove the fallback Headers() construction. Do NOT remove the[trpc] createContext called WITHOUT resHeaderswarning log. -
apps/web/src/app/api/trpc/[trpc]/route.ts— WrapsfetchRequestHandlerand intercepts the tRPC response. For each item in the response body, it drills intoresult.data.json.__setCookie, appends any found cookie strings as realSet-Cookieheaders on a new Response, and removes the__setCookiefield from the body before returning to the client. Do NOT simplify this handler. Do NOT "refactor" the response interception into middleware. Do NOT switch the handler to edge runtime — it MUST stay onnodejsruntime.
If a future Next.js or tRPC version fixes the underlying bug, the workaround can be removed — but only after a user confirms with curl testing that ctx.resHeaders.append('Set-Cookie', ...) directly in a login procedure sets a real Set-Cookie header in the production container. Until then, the three-file pattern stays.
Reference commit: 1c9bf69 — fix(auth): work around Next.js standalone tRPC resHeaders bug
Every change you make MUST follow this process:
- Read the spec handed to you — if there's no spec, ask for one. Don't freelance.
- Read the files you plan to modify before editing.
- Scope only what the spec asks for — don't refactor, don't "clean up," don't fix adjacent bugs unless the spec says so.
- Run the verification gates the spec provides. If there are no gates, write them with the user before coding.
- One commit per spec with a descriptive message. No "fix stuff + other tweaks" commits.
- Report back in the format the spec requests. If no format, report:
- Files modified (with line counts)
- Commit hash
- What gates passed
- What you deliberately did NOT change but noticed
If the user asks you to fix Bug X and you spot Bug Y, note Bug Y in your report but do NOT fix it. Scope creep has broken past deployments.
Legacy code that works is not a bug. Rewriting working code for style or taste without being asked is forbidden.
If the user's Prettier config produces weird indentation, respect it. Don't "fix" formatting as a side effect. Use --no-format patches if available.
New packages get added ONLY with user approval. pnpm add <anything> is a decision, not a reflex.
This project doesn't currently have comprehensive test coverage. Adding tests for new code is fine; adding tests for existing code as a side quest is not. The user will decide when to invest in test coverage.
If a dependency is pinned to 1.23.4, don't bump it to 1.23.5 or ^1.23.4 unless the user asks. Version bumps in dependencies have broken the build before.
They're there for a reason. If you think one should be addressed, mention it in your report. Don't silently delete context.
The TypeScript compiler can emit .js shadows that override .ts sources at runtime because package.json exports reference paths without extensions. Committing these files silently breaks every future edit to the corresponding .ts file. This cost a full day of debugging on 2026-04-18 (see commit 6820364).
Hard rules:
- Never run
tscin this repo without--noEmit - Never commit files matching
packages/*/src/**/*.js - If you see
.jsfiles next to.tsfiles inpackages/*/src/, delete them immediately - The build check in
scripts/check-no-js-shadows.shenforces this at Docker build time - The
.gitignoreblocks new commits of these files
If a build fails with a shadow-guard error, DO NOT bypass the check — find and delete the shadow files, or ask the user if you believe the guard is wrong.
- Ask clarifying questions before writing code. Better to ask one "is this what you meant?" than to rewrite 500 lines on a misread spec.
- Read the relevant spec files in
/mnt/user-data/outputs/when referenced — the user's specs are canonical. - Propose a plan before coding if the task is longer than 50 lines of change.
- Run the existing test suite before declaring done:
pnpm test(even if you didn't add tests, don't break them). - Match the existing code style — imitate, don't innovate.
ProxyOS is part of the Homelab OS product family. Other products in the family share patterns — apps/web layout, Caddy wrapping pattern, tRPC API layout, better-auth for auth, Lemon Squeezy billing. If you're asked to add a feature that looks similar to something in sibling products (BackupOS, InfraOS, MxWatch, LockBoxOS, AccessOS), check if there's a shared package in packages/ first before reinventing.
ProxyOS's special responsibility: it's the reverse proxy. Routes are user-facing. Breaking a route in production means users can't reach their services. Treat the route generator and Caddy config layer with extra caution.
If you encounter any of these, STOP and ask before proceeding:
- User asks you to modify a file in the locked list above
- A change you're about to make would require updating the locked list
- The spec you were given appears internally contradictory
- A verification gate defined in the spec is failing and you can't see why
- You find a security-relevant issue (auth bypass, SQL injection, exposed secret)
- You're about to add a new top-level dependency
- You're about to change database schema in a way that requires data migration
For all of these: STOP. Report the finding. Wait for user direction.
The routes.sync_source field records which flow last pushed a route to Caddy. Verification uses it to decide whether drift is a real alert ('drift') or expected ('synced-machine'):
'manual'— user-driven create/update/expose; drift = red alert'bootstrap'— startup replaceRoutes; drift = red alert (should match buildCaddyRoute exactly)'drift-repair'— user clicked Re-push; drift = red alert'patchos'— PatchOS maintenance push (partial route); drift = expected, grey status'scheduled'— scheduled-changes push; drift = expected, grey statusnull— unknown; fail-closed to'drift'
If you add a new flow that pushes routes to Caddy, it MUST set syncSource after the push, using one of the values above or adding a new value (and updating classifyDrift in packages/caddy/src/verify.ts to handle it). Forgetting to set sync_source is a bug.
2026-04-18 Initial version — after Phase 1 ProxyOS bug fixes
Locked: next.config.mjs, build-route.ts, resolve-upstream.ts,
regenerate-routes.ts, all existing migrations
2026-04-18 Added Auth Cookie Handling lock (commit 1c9bf69)
Locked: packages/api/src/routers/users.ts (login __setCookie),
packages/api/src/trpc.ts (resHeaders fallback),
apps/web/src/app/api/trpc/[trpc]/route.ts (interceptor),
docker-compose.yml (docker.sock mount)
2026-04-18 Fix 4: roundtrip verification (commits 32c556d, 07d8255, c68a7ad, 2f39841, 273cc10)
Added: verify.ts (diffCaddyRoute + classifyDrift), CaddyClient.verifyRoute,
verifyAndPersist helper in routes.ts, forceResync procedure,
sync_status/sync_diff/sync_checked_at/sync_source columns on routes,
dashboard Sync column + drift banner with Re-push button
Locked: packages/caddy/src/verify.ts, Fix 4 entries in migrations.ts
Policy: sync_source must be set by every route-push flow; classifyDrift
must be updated if a new sync_source value is introduced
2026-04-22 Stability Engineering Initiative v0.1.0 (PRs #1–#4, spec: Specs/proxyos-stability-engineering-spec.md)
Phase 2: withCaddySync<T>() two-phase commit wraps all route mutations
Phase 3: all mutations return route; verifyRoute post-save; optimistic UI rollback; form reset
Phase 4: applyDockerDns() wrapper (NOT touching locked config.ts); WAL checkpoint;
federation error clarity; rebuild smoke test
Phase 5: property-based tests (fast-check), migration tests, round-trip tests,
tRPC mutation tests, Playwright E2E, CI workflow (.github/workflows/ci.yml)
Phase 6: routes.testUpstream probe (7 error categories); server-side pagination
(routes.list limit/offset + routes.count); Caddy retry exponential backoff;
removed dormant trustUpstreamHeaders UI toggle; browser compat doc
New files (NOT locked): packages/caddy/src/apply-docker-dns.ts,
packages/caddy/src/__tests__/property.test.ts,
packages/caddy/src/__tests__/round-trip.test.ts,
packages/db/src/__tests__/migrations.test.ts,
packages/api/src/__tests__/routes.test.ts,
apps/web/tests/e2e/routes.spec.ts,
.github/workflows/ci.yml,
scripts/rebuild-smoke-test.sh
When you update this file (with user approval), append a version row.
- Read this file every session.
- Don't touch locked files without explicit user ask.
- Don't expand scope.
- Don't refactor what works.
- Run the gates the spec gives you.
- One commit per spec.
- Report honestly — including what you didn't do.
This isn't bureaucracy. It's because the user has been burned by scope creep and silent changes. Respect the contract and this project stays shippable.