Skip to content

Commit b29cc02

Browse files
BunsDevclaude
andcommitted
Add blur protection for .env file contents in code viewer
Show .env and .env.local files in the file tree (even when gitignored) and blur their contents by default with a toggle to reveal values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 679926b commit b29cc02

2 files changed

Lines changed: 83 additions & 12 deletions

File tree

apps/server/src/workspaceEntries.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,23 @@ async function mapWithConcurrency<TInput, TOutput>(
686686
return results;
687687
}
688688

689+
const ENV_FILE_PATTERN = /^\.env(\..+)?$/;
690+
691+
/**
692+
* Discover .env* files at the workspace root so they appear in the file tree
693+
* even when gitignored. Only root-level env files are included.
694+
*/
695+
async function discoverEnvFiles(cwd: string): Promise<string[]> {
696+
try {
697+
const dirEntries = await fs.readdir(cwd, { withFileTypes: true });
698+
return dirEntries
699+
.filter((entry) => entry.isFile() && ENV_FILE_PATTERN.test(entry.name))
700+
.map((entry) => entry.name);
701+
} catch {
702+
return [];
703+
}
704+
}
705+
689706
async function isInsideGitWorkTree(cwd: string): Promise<boolean> {
690707
const insideWorkTree = await runProcess("git", ["rev-parse", "--is-inside-work-tree"], {
691708
cwd,
@@ -799,6 +816,14 @@ async function buildWorkspaceIndexFromGit(cwd: string): Promise<WorkspaceIndex |
799816
.filter((entry) => entry.length > 0 && !isPathInIgnoredDirectory(entry));
800817
const filePaths = await filterGitIgnoredPaths(cwd, listedPaths);
801818

819+
// Include .env* files that may be gitignored so they appear in the file tree
820+
const envFiles = await discoverEnvFiles(cwd);
821+
for (const envPath of envFiles) {
822+
if (!filePaths.includes(envPath)) {
823+
filePaths.push(envPath);
824+
}
825+
}
826+
802827
const directorySet = new Set<string>();
803828
for (const filePath of filePaths) {
804829
for (const directoryPath of directoryAncestorsOf(filePath)) {

apps/web/src/components/CodeViewerPanel.tsx

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useQuery } from "@tanstack/react-query";
2-
import { FileCodeIcon, XIcon } from "lucide-react";
3-
import { memo, useCallback } from "react";
2+
import { EyeIcon, EyeOffIcon, FileCodeIcon, XIcon } from "lucide-react";
3+
import { memo, useCallback, useState } from "react";
44

55
import { useCodeViewerStore, type CodeViewerTab } from "~/codeViewerStore";
66
import { useTheme } from "~/hooks/useTheme";
@@ -13,6 +13,12 @@ import { MarkdownPreview } from "./MarkdownPreview";
1313
import { isElectron } from "~/env";
1414
import { Button } from "./ui/button";
1515

16+
/** Check if a file path is a dotenv / secrets file whose values should be masked. */
17+
function isEnvFile(filePath: string): boolean {
18+
const basename = filePath.split("/").pop() ?? filePath;
19+
return /^\.env(\..*)?$/.test(basename);
20+
}
21+
1622
export function CodeViewerTabStrip(props: {
1723
tabs: CodeViewerTab[];
1824
activeTabPath: string | null;
@@ -66,6 +72,9 @@ export const CodeViewerFileContent = memo(function CodeViewerFileContent(props:
6672
resolvedTheme: "light" | "dark";
6773
onAddContext: (ctx: CodeContextSelection) => void;
6874
}) {
75+
const envFile = isEnvFile(props.relativePath);
76+
const [envValuesRevealed, setEnvValuesRevealed] = useState(false);
77+
6978
const query = useQuery(
7079
projectReadFileQueryOptions({
7180
cwd: props.cwd,
@@ -109,22 +118,59 @@ export const CodeViewerFileContent = memo(function CodeViewerFileContent(props:
109118
}
110119

111120
return (
112-
<div className="min-h-0 flex-1 overflow-y-auto">
121+
<div className="relative min-h-0 flex-1 overflow-y-auto">
113122
{query.data.truncated && (
114123
<div className="border-b border-amber-500/30 bg-amber-500/10 px-3 py-1 text-[11px] text-amber-700 dark:text-amber-300/90">
115124
File is larger than 1MB. Showing truncated content.
116125
</div>
117126
)}
118-
{isMarkdownPreviewFilePath(props.relativePath) ? (
119-
<MarkdownPreview contents={query.data.contents} />
120-
) : (
121-
<CodeMirrorViewer
122-
contents={query.data.contents}
123-
filePath={props.relativePath}
124-
resolvedTheme={props.resolvedTheme}
125-
onAddContext={props.onAddContext}
126-
/>
127+
128+
{/* Env file: show/hide toggle banner */}
129+
{envFile && (
130+
<div className="sticky top-0 z-10 flex items-center justify-between border-b border-amber-500/30 bg-amber-500/10 px-3 py-1.5">
131+
<span className="text-[11px] font-medium text-amber-700 dark:text-amber-300/90">
132+
Sensitive file — values are hidden by default
133+
</span>
134+
<Button
135+
type="button"
136+
size="xs"
137+
variant="ghost"
138+
className="gap-1.5 text-[11px] text-amber-700 hover:text-amber-900 dark:text-amber-300/90 dark:hover:text-amber-100"
139+
onClick={() => setEnvValuesRevealed((prev) => !prev)}
140+
>
141+
{envValuesRevealed ? (
142+
<>
143+
<EyeOffIcon className="size-3.5" />
144+
Hide values
145+
</>
146+
) : (
147+
<>
148+
<EyeIcon className="size-3.5" />
149+
Show values
150+
</>
151+
)}
152+
</Button>
153+
</div>
127154
)}
155+
156+
<div
157+
className={cn(
158+
envFile &&
159+
!envValuesRevealed &&
160+
"select-none blur-[6px] transition-[filter] duration-200",
161+
)}
162+
>
163+
{isMarkdownPreviewFilePath(props.relativePath) ? (
164+
<MarkdownPreview contents={query.data.contents} />
165+
) : (
166+
<CodeMirrorViewer
167+
contents={query.data.contents}
168+
filePath={props.relativePath}
169+
resolvedTheme={props.resolvedTheme}
170+
onAddContext={props.onAddContext}
171+
/>
172+
)}
173+
</div>
128174
</div>
129175
);
130176
});

0 commit comments

Comments
 (0)