Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/claude_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,11 @@ def _map_decision(self, kind: str, tool_name: str,
if tool_name == "ExitPlanMode":
# Plan approval isn't binary: "auto"/"manual"/"proceed" leave plan
# mode; anything else keeps planning and forwards the text as feedback.
# Fail closed: an explicit decline wins even when the phrase also
# contains an intent word — "do not proceed" / "don't approve" must
# NOT be read as approval just because they contain "proceed"/"approve".
if decide_permission(choice) == "deny":
return sdk.PermissionResultDeny(message=f"Keep planning: {choice}")
if (decide_permission(choice) == "allow"
or any(w in c for w in ("auto", "manual", "proceed", "approve"))):
return sdk.PermissionResultAllow()
Expand Down
7 changes: 7 additions & 0 deletions backend/tmux_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,13 @@ def _classify_plan_choice(self, c: str) -> str:
(3. refine with Ultraplan on the web), or 'decline' (stay in plan mode,
forward any feedback). Order matters: 'manually approve' contains an
approve-word, so the more specific intents are checked first."""
# Fail closed: an explicit decline wins even when the phrase also contains
# an intent substring — "do not approve edits" / "don't switch to auto"
# must decline, not approve. decide_permission resolves genuine approvals
# ("manually approve edits") to "allow", so the intent paths below are
# unchanged; only true negations are short-circuited here.
if decide_permission(c) == "deny":
return "decline"
if "manual" in c or "approve edit" in c or "each edit" in c or "review edit" in c:
return "manual"
if "ultraplan" in c or "on the web" in c or "refine on" in c:
Expand Down
18 changes: 18 additions & 0 deletions frontend/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ const nextConfig = {
// that bypass the same-origin proxy. Defaults to the standard backend port.
BACKEND_PORT: process.env.BACKEND_PORT || "8000",
},
// Security response headers for every route. This is an open same-origin proxy
// into a command-executing backend (and binds 0.0.0.0 in network mode), so deny
// framing (clickjacking of the voice-activate / on-screen controls), forbid
// MIME sniffing, and suppress referrer leakage. CSRF is separately handled by
// the Sec-Fetch-Site check in lib/proxyAuth.ts.
async headers() {
return [
{
source: "/:path*",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Content-Security-Policy", value: "frame-ancestors 'none'" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "no-referrer" },
],
},
];
},
// Allow loading the dev server's /_next/* resources (JS chunks, HMR) when the
// app is opened from another device on the LAN — otherwise Next 16 blocks them
// as cross-origin and the client never hydrates (toggles/buttons do nothing).
Expand Down