Skip to content

Commit 31577ea

Browse files
Merge pull request #9 from easyvibecoding/feat/terminal-masking
feat: add experimental Shielded Terminal for API key masking in VS Code
2 parents 1c4a7f5 + 01a8501 commit 31577ea

8 files changed

Lines changed: 712 additions & 2 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ These are absolute rules — never violate them:
103103
- **Clipboard capture whitespace stripping**: `detectKeysInClipboard()` strips all whitespace and newlines from clipboard content before pattern matching (`components(separatedBy: .whitespacesAndNewlines).joined()`). API keys never contain spaces, but clipboard copy may introduce line breaks from word-wrap.
104104
- **Clipboard capture built-in patterns**: `ClipboardEngine.builtInCapturePatterns` contains 22 well-known API key patterns (OpenAI, Anthropic, GitHub, AWS, Google, Stripe, etc.) for detecting NEW keys not yet in the vault. This is separate from `patternCacheEntries()` which only matches stored keys.
105105
- **NSAlert in menu bar app**: Must call `alert.layout()` then set `alert.window.level = .floating` + `orderFrontRegardless()` before `runModal()`. Without this, the alert either doesn't appear or creates a dock icon. Do NOT use `setActivationPolicy(.regular)`.
106+
- **Terminal masking sync block buffering** (experimental): Shielded Terminal buffers PTY output by detecting DEC 2026 sync block markers (`\x1b[?2026h`/`\x1b[?2026l`). Complete sync blocks are masked atomically. Non-sync data uses 30ms timeout buffer. This matches claude-chill's approach.
107+
- **Terminal masking ANSI-aware matching**: `maskTerminalOutput()` strips ANSI escape codes AND all whitespace (spaces, tabs, newlines) before regex matching. Ink word-wraps long keys with `\r\n` + indentation; stripping all whitespace allows regex to match keys across visual line breaks. Structural characters (ANSI + whitespace) within matched ranges are preserved in output via `extractStructural()`.
108+
- **Terminal masking node-pty loading**: Triple fallback: (1) `require('node-pty')`, (2) `require(vscode.env.appRoot + '/node_modules.asar.unpacked/node-pty')`, (3) `require(vscode.env.appRoot + '/node_modules/node-pty')`. Falls back to `child_process.spawn` line-mode terminal.
106109

107110
## Documentation
108111

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ See the [docs/](docs/) directory for detailed specifications:
168168
- [x] Smart Key Extraction confirmation dialog (full Chrome ↔ Swift Core IPC: detect → submit → Keychain store → pattern sync)
169169
- [x] Linked Key Groups (sequential paste with ⌘V→Tab automation, Settings UI CRUD, pre-fetch Keychain)
170170
- [x] Clipboard Capture (⌃⌥⌘V hotkey, 22 built-in patterns, 3-tier confidence routing, whitespace-tolerant)
171+
- [x] 🧪 Terminal masking — Shielded Terminal (node-pty proxy, DEC 2026 sync block buffering, ANSI-aware masking, Claude Code compatible)
171172
- [ ] API Key rotation & deployment sync
172-
- [ ] Terminal masking (node-pty proxy)
173173
- [ ] System-wide masking (Accessibility API)
174174

175175
## Contributing

docs/01-product-spec/implementation-status.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
|------|--------------|---------|---------|
3535
| ~~`⌃⌥⌘V` capture clipboard~~ | Spec §4.4 | ~~~~ | ✅ HotkeyManager → ClipboardEngine.detectKeysInClipboard() → 內建 22 種 pattern 偵測 → confidence 三階路由 → VaultManager 存儲 |
3636
| ~~Linked Key Groups (sequential paste)~~ | Spec §6.3 | ~~~~ |`LinkedGroup`/`GroupEntry`/`SequentialPasteEngine` 完成、Settings UI 群組管理(CRUD)、`request_paste_group` IPC handler |
37+
| ~~Terminal masking (Shielded Terminal)~~ | Spec §3.2 | ~~~~ | 🧪 實驗性。node-pty proxy + DEC 2026 sync block buffering + ANSI-aware masking。Known limitation: Rewind 確認頁部分洩漏 |
3738
| Shortcut conflict detection | Spec §4.4 || |
3839
| Import / Export vault | Spec §9.1 || |
3940

docs/en/01-product-spec/implementation-status.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
|---------|-------------|----------|---------|
3535
| ~~`⌃⌥⌘V` capture clipboard~~ | Spec §4.4 | ~~Medium~~ | ✅ HotkeyManager → ClipboardEngine.detectKeysInClipboard() → 22 built-in patterns → 3-tier confidence routing → VaultManager store |
3636
| ~~Linked Key Groups (sequential paste)~~ | Spec §6.3 | ~~Medium~~ |`LinkedGroup`/`GroupEntry`/`SequentialPasteEngine` complete, Settings UI group management (CRUD), `request_paste_group` IPC handler |
37+
| ~~Terminal masking (Shielded Terminal)~~ | Spec §3.2 | ~~Medium~~ | 🧪 Experimental. node-pty proxy + DEC 2026 sync block buffering + ANSI-aware masking. Known limitation: Rewind confirmation page partial leak |
3738
| Shortcut conflict detection | Spec §4.4 | Low | |
3839
| Import / Export vault | Spec §9.1 | Low | |
3940

packages/vscode-extension/package.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@
2626
"command": "demosafe.pasteKey",
2727
"title": "DemoSafe: Paste Key",
2828
"icon": "$(key)"
29+
},
30+
{
31+
"command": "demosafe.openShieldedTerminal",
32+
"title": "DemoSafe: Open Shielded Terminal",
33+
"icon": "$(shield)"
34+
},
35+
{
36+
"command": "demosafe.openTerminalWithCommand",
37+
"title": "DemoSafe: Open Terminal With Command",
38+
"icon": "$(terminal)"
2939
}
3040
],
3141
"keybindings": [
@@ -38,11 +48,16 @@
3848
"command": "demosafe.pasteKey",
3949
"key": "ctrl+alt+space",
4050
"mac": "ctrl+alt+space"
51+
},
52+
{
53+
"command": "demosafe.openShieldedTerminal",
54+
"key": "ctrl+shift+t",
55+
"mac": "ctrl+shift+t"
4156
}
4257
]
4358
},
4459
"scripts": {
45-
"build": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node",
60+
"build": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --external:node-pty --format=cjs --platform=node",
4661
"watch": "npm run build -- --watch",
4762
"type-check": "tsc --noEmit",
4863
"lint": "eslint --ext .ts src/",

packages/vscode-extension/src/extension.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ import { StatusBarManager, ConnectionState } from './statusbar/statusbar-manager
55
import { PatternScanner } from './core/pattern-scanner';
66
import { PatternCache } from './core/pattern-cache';
77
import { pasteKeyCommand } from './commands/paste-key';
8+
import { ShieldedTerminal, FallbackShieldedTerminal } from './terminal/shielded-terminal';
9+
import { buildTerminalPatterns } from './terminal/terminal-patterns';
810

911
let ipcClient: IPCClient;
1012
let decorationManager: DecorationManager;
1113
let statusBarManager: StatusBarManager;
1214
let patternScanner: PatternScanner;
1315
let patternCache: PatternCache;
1416
let isDemoMode = false;
17+
let isShieldActive = false;
1518
let outputChannel: vscode.OutputChannel;
1619

20+
// Track all shielded terminals for state sync
21+
const shieldedTerminals: Set<ShieldedTerminal | FallbackShieldedTerminal> = new Set();
22+
1723
export function activate(context: vscode.ExtensionContext) {
1824
outputChannel = vscode.window.createOutputChannel('DemoSafe');
1925
outputChannel.appendLine('[DemoSafe] Extension activating...');
@@ -38,6 +44,19 @@ export function activate(context: vscode.ExtensionContext) {
3844
vscode.commands.registerCommand('demosafe.pasteKey', () => {
3945
pasteKeyCommand(patternCache, ipcClient);
4046
}),
47+
vscode.commands.registerCommand('demosafe.openShieldedTerminal', () => {
48+
openShieldedTerminal();
49+
}),
50+
vscode.commands.registerCommand('demosafe.openTerminalWithCommand', async () => {
51+
const cmd = await vscode.window.showInputBox({
52+
prompt: 'Command to run in shielded terminal',
53+
placeHolder: 'e.g. claude, npm start, python app.py',
54+
value: 'claude',
55+
});
56+
if (cmd) {
57+
openShieldedTerminal(cmd);
58+
}
59+
}),
4160
);
4261

4362
// IPC log forwarding
@@ -59,6 +78,8 @@ export function activate(context: vscode.ExtensionContext) {
5978

6079
ipcClient.on('stateChanged', (state: { isDemoMode: boolean; activeContext: { name: string } | null }) => {
6180
isDemoMode = state.isDemoMode;
81+
isShieldActive = state.isDemoMode; // Sync shield with Demo Mode
82+
syncShieldState();
6283
statusBarManager.update({
6384
isDemoMode: state.isDemoMode,
6485
contextName: state.activeContext?.name ?? null,
@@ -68,6 +89,11 @@ export function activate(context: vscode.ExtensionContext) {
6889

6990
ipcClient.on('patternsUpdated', () => {
7091
updateMasking();
92+
// Update terminal patterns when Core syncs new patterns
93+
const patterns = buildTerminalPatterns(patternCache);
94+
for (const t of shieldedTerminals) {
95+
t.updatePatterns(patterns);
96+
}
7197
});
7298

7399
ipcClient.on('clipboardCleared', () => {
@@ -153,7 +179,57 @@ function updateConnectionState(state: ConnectionState) {
153179
}
154180
}
155181

182+
// --- Shielded Terminal ---
183+
184+
function openShieldedTerminal(initialCommand?: string) {
185+
const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.env.HOME || '/';
186+
const patterns = buildTerminalPatterns(patternCache);
187+
188+
// Shielded Terminal always starts with shield ON — that's its purpose
189+
const shieldOn = true;
190+
let pty: ShieldedTerminal | FallbackShieldedTerminal;
191+
let terminalName: string;
192+
193+
try {
194+
pty = new ShieldedTerminal(cwd, shieldOn, patterns, outputChannel);
195+
terminalName = 'Shield';
196+
} catch {
197+
pty = new FallbackShieldedTerminal(cwd, shieldOn, patterns);
198+
terminalName = 'Shield (Fallback)';
199+
}
200+
201+
shieldedTerminals.add(pty);
202+
203+
const terminal = vscode.window.createTerminal({
204+
name: terminalName,
205+
pty,
206+
location: vscode.TerminalLocation.Editor,
207+
});
208+
209+
terminal.show();
210+
211+
// Send initial command after shell starts
212+
if (initialCommand) {
213+
setTimeout(() => {
214+
for (const char of initialCommand) {
215+
pty.handleInput(char);
216+
}
217+
pty.handleInput('\r');
218+
}, 500);
219+
}
220+
}
221+
222+
function syncShieldState() {
223+
for (const t of shieldedTerminals) {
224+
t.shieldEnabled = isShieldActive;
225+
}
226+
}
227+
156228
export function deactivate() {
229+
for (const t of shieldedTerminals) {
230+
t.close();
231+
}
232+
shieldedTerminals.clear();
157233
ipcClient?.disconnect();
158234
decorationManager?.dispose();
159235
statusBarManager?.dispose();

0 commit comments

Comments
 (0)