diff --git a/strix/config/config.py b/strix/config/config.py
index 4cf6e600..6f98d920 100644
--- a/strix/config/config.py
+++ b/strix/config/config.py
@@ -141,11 +141,10 @@ class Config:
cls.save({"env": env_vars})
applied = {}
- llm_vars = cls._llm_env_vars()
for var_name, var_value in env_vars.items():
if var_name in cls.tracked_vars():
- # LLM vars in cli-config.json always win over shell env
- if var_name in llm_vars or force or var_name not in os.environ:
+ # Shell env wins unless --force or the var is not set in shell.
+ if force or var_name not in os.environ:
os.environ[var_name] = var_value
applied[var_name] = var_value
diff --git a/strix/interface/checkpoint_restore.py b/strix/interface/checkpoint_restore.py
index 5a9f1dc0..b4a2f830 100644
--- a/strix/interface/checkpoint_restore.py
+++ b/strix/interface/checkpoint_restore.py
@@ -34,12 +34,11 @@ def restore_sub_agents(checkpoint_data: Any, llm_config: Any) -> list[str]:
def _depth(aid: str) -> int:
if aid in _memo:
return _memo[aid]
+ # Mark before recursing to break any cycle in corrupted checkpoints.
+ _memo[aid] = 0
parent = sub_agent_states.get(aid, {}).get("parent_id")
- _memo[aid] = (
- 0
- if (parent is None or parent not in sub_agent_states)
- else 1 + _depth(parent)
- )
+ if parent is not None and parent in sub_agent_states:
+ _memo[aid] = 1 + _depth(parent)
return _memo[aid]
restored_ids: list[str] = []
diff --git a/strix/interface/cli.py b/strix/interface/cli.py
index 3950e693..7bf88f91 100644
--- a/strix/interface/cli.py
+++ b/strix/interface/cli.py
@@ -266,9 +266,13 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
return
if _checkpoint_saved.is_set():
return
+ agent_instance = _agent_ref[0]
+ # Skip if the scan already completed successfully — the checkpoint
+ # was deleted in base_agent.py and there is nothing to resume.
+ if getattr(agent_instance.state, "completed", False):
+ return
_checkpoint_saved.set()
try:
- agent_instance = _agent_ref[0]
checkpoint_manager.save(
agent_instance.state,
tracer,
diff --git a/strix/interface/tui.py b/strix/interface/tui.py
index 09b048f8..c30cca7f 100644
--- a/strix/interface/tui.py
+++ b/strix/interface/tui.py
@@ -814,6 +814,9 @@ class StrixTUIApp(App): # type: ignore[misc]
return
if _checkpoint_saved.is_set():
return
+ # Skip if the scan already completed — nothing to resume.
+ if getattr(agent.state, "completed", False):
+ return
_checkpoint_saved.set()
try:
mgr.save(
@@ -2021,14 +2024,16 @@ class StrixTUIApp(App): # type: ignore[misc]
_agent = getattr(self, "_current_agent", None)
if _mgr and _agent:
import contextlib
- with contextlib.suppress(Exception):
- _mgr.save(
- _agent.state,
- self.tracer,
- self.scan_config,
- self.agent_config.get("target_hash", ""),
- _agent.max_iterations,
- )
+ # Only save if the scan was interrupted, not if it finished cleanly.
+ if not getattr(_agent.state, "completed", False):
+ with contextlib.suppress(Exception):
+ _mgr.save(
+ _agent.state,
+ self.tracer,
+ self.scan_config,
+ self.agent_config.get("target_hash", ""),
+ _agent.max_iterations,
+ )
self.tracer.cleanup()
diff --git a/strix/skills/vulnerabilities/clickjacking.md b/strix/skills/vulnerabilities/clickjacking.md
new file mode 100644
index 00000000..18b4219c
--- /dev/null
+++ b/strix/skills/vulnerabilities/clickjacking.md
@@ -0,0 +1,71 @@
+---
+name: clickjacking
+description: Clickjacking testing covering UI redressing, frame embedding, and X-Frame-Options / CSP bypass techniques
+---
+
+# Clickjacking
+
+Clickjacking (UI redressing) tricks users into clicking hidden or disguised UI elements by overlaying transparent iframes on top of legitimate pages.
+
+## Attack Surface
+
+**Targets**
+- Pages that perform sensitive actions (fund transfers, account changes, password resets, OAuth authorization, social actions)
+- Pages missing `X-Frame-Options` or `Content-Security-Policy: frame-ancestors`
+
+**Defenses to Bypass**
+- `X-Frame-Options: DENY / SAMEORIGIN`
+- `Content-Security-Policy: frame-ancestors 'none' / 'self'`
+- Frame-busting JavaScript
+
+## Testing Methodology
+
+### Step 1 – Check Headers
+```
+curl -s -I https://target.com | grep -i "x-frame-options\|frame-ancestors"
+```
+Missing or misconfigured headers indicate framing is allowed.
+
+### Step 2 – Attempt Embedding
+```html
+
+```
+If the page renders inside the iframe, the site is vulnerable.
+
+### Step 3 – Frame-Buster Bypass
+If JavaScript frame-busting is used (e.g., `if (top !== self) top.location = self.location`):
+- Use `sandbox` attribute to disable JS: `