diff --git a/frontend/src/components/StackAnatomyPanel.tsx b/frontend/src/components/StackAnatomyPanel.tsx
index 5a5a38ac..89bcf00e 100644
--- a/frontend/src/components/StackAnatomyPanel.tsx
+++ b/frontend/src/components/StackAnatomyPanel.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react';
-import { GitBranch, Pencil, ExternalLink, Rocket, FolderOpen } from 'lucide-react';
+import { GitBranch, Pencil, ExternalLink, Rocket, FolderOpen, X } from 'lucide-react';
import { Button } from './ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { ScrollableTabRow } from './ui/ScrollableTabRow';
@@ -7,6 +7,7 @@ import { apiFetch } from '@/lib/api';
import { cn } from '@/lib/utils';
import { type AnatomyMarkdownInput, type PortRow, type VolumeRow } from '@/lib/anatomyMarkdown';
import { usePreflightDismiss } from '@/hooks/usePreflightDismiss';
+import { useScanBannerDismiss } from '@/hooks/useScanBannerDismiss';
import { parseAnatomy, parseEnvKeys, formatGitSource, primaryPublishedHostPort, type GitSourceInfo } from '@/lib/anatomy';
import { buildServiceUrl } from '@/lib/serviceUrl';
import { StackActivityTimeline } from './stack/StackActivityTimeline';
@@ -126,6 +127,8 @@ export default function StackAnatomyPanel({
attemptedAt?: number;
errorMessage?: string | null;
} | null>(null);
+ const { dismissed: scanBannerDismissed, dismiss: dismissScanBanner } =
+ useScanBannerDismiss(stackName, activeNode?.id, scanStatus);
// Best-effort badge: read the last stored preflight severity to dot the tab.
// Skipped when the active node does not advertise the capability.
@@ -570,7 +573,7 @@ export default function StackAnatomyPanel({
)}
- {scanStatus && scanStatus.status && scanStatus.status !== 'ok' && (
+ {scanStatus && scanStatus.status && scanStatus.status !== 'ok' && !scanBannerDismissed && (
+
)}
diff --git a/frontend/src/hooks/useScanBannerDismiss.ts b/frontend/src/hooks/useScanBannerDismiss.ts
new file mode 100644
index 00000000..d0494635
--- /dev/null
+++ b/frontend/src/hooks/useScanBannerDismiss.ts
@@ -0,0 +1,60 @@
+import { useCallback, useEffect, useMemo, useState } from 'react';
+
+// Bumped when a dismiss is written so sibling consumers re-read localStorage
+// and agree, without a full page reload.
+const DISMISS_EVENT = 'sencho:scan-banner-dismiss-changed';
+
+const keyFor = (stackName: string, nodeId: number | undefined) =>
+ `sencho.scanBannerDismissed.${stackName}.${nodeId ?? 'local'}`;
+
+/** Fingerprint encodes the scan outcome: any new scan run (different attemptedAt)
+ * or status change produces a new fingerprint, re-surfacing the banner. */
+function fingerprint(status: string | null, attemptedAt: number | undefined): string {
+ if (!status) return '';
+ return `${status}:${attemptedAt ?? 0}`;
+}
+
+/**
+ * Per-stack dismiss for the post-deploy scan warning banner, persisted in
+ * localStorage and keyed to a fingerprint of the scan run. Dismissal sticks
+ * across reloads for the same scan outcome, and clears automatically once a
+ * new scan runs (different attemptedAt) or the status changes.
+ */
+export function useScanBannerDismiss(
+ stackName: string,
+ nodeId: number | undefined,
+ scanStatus: { status: string | null; attemptedAt?: number } | null,
+) {
+ const fp = useMemo(
+ () => fingerprint(scanStatus?.status ?? null, scanStatus?.attemptedAt),
+ [scanStatus?.status, scanStatus?.attemptedAt],
+ );
+ const storageKey = keyFor(stackName, nodeId);
+
+ const read = useCallback(() => {
+ try { return localStorage.getItem(storageKey); } catch { return null; }
+ }, [storageKey]);
+
+ const [storedFp, setStoredFp] = useState(() => read());
+
+ useEffect(() => {
+ setStoredFp(read());
+ const handler = () => setStoredFp(read());
+ window.addEventListener(DISMISS_EVENT, handler);
+ window.addEventListener('storage', handler);
+ return () => {
+ window.removeEventListener(DISMISS_EVENT, handler);
+ window.removeEventListener('storage', handler);
+ };
+ }, [read]);
+
+ const dismissed = fp !== '' && storedFp === fp;
+
+ const dismiss = useCallback(() => {
+ try { localStorage.setItem(storageKey, fp); } catch { /* ignore */ }
+ setStoredFp(fp);
+ window.dispatchEvent(new Event(DISMISS_EVENT));
+ }, [storageKey, fp]);
+
+ return { dismissed, dismiss };
+}