[codex] Hide managed startup commands from history

Hide Netcatty-managed Docker and tmux terminal launch commands from command history.

Validated locally with lint, full tests, and build. Multi-agent review completed with no remaining issues.
This commit is contained in:
陈大猫
2026-06-12 14:00:16 +08:00
committed by GitHub
parent 17c8f11194
commit 46b9bf6ccb
7 changed files with 188 additions and 8 deletions

View File

@@ -0,0 +1,53 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { buildDockerLogsCommand } from '../../domain/systemManager/dockerShell.ts';
import { loadSanitizedShellHistory } from './shellHistoryPersistence.ts';
import type { ShellHistoryEntry } from '../../domain/models.ts';
const entry = (id: string, command: string): ShellHistoryEntry => ({
id,
command,
hostId: 'host-1',
hostLabel: 'Host',
sessionId: 'session-1',
timestamp: 1000,
});
test('loadSanitizedShellHistory removes persisted managed startup commands and writes back cleaned history', () => {
const stored = [
entry('managed', buildDockerLogsCommand('587abcdef123')),
entry('user', 'docker ps -a'),
];
let written: ShellHistoryEntry[] | null = null;
const loaded = loadSanitizedShellHistory({
read: () => stored,
write: (_key, value) => {
written = value;
return true;
},
});
assert.deepEqual(
loaded?.map((item) => item.command),
['docker ps -a'],
);
assert.deepEqual(written, loaded);
});
test('loadSanitizedShellHistory does not write when persisted history is already clean', () => {
const stored = [entry('user', 'docker ps -a')];
let writeCount = 0;
const loaded = loadSanitizedShellHistory({
read: () => stored,
write: () => {
writeCount += 1;
return true;
},
});
assert.deepEqual(loaded, stored);
assert.equal(writeCount, 0);
});

View File

@@ -0,0 +1,23 @@
import type { ShellHistoryEntry } from '../../domain/models';
import { sanitizeGlobalHistoryEntries } from '../../domain/globalHistory';
import { STORAGE_KEY_SHELL_HISTORY } from '../../infrastructure/config/storageKeys';
import { localStorageAdapter } from '../../infrastructure/persistence/localStorageAdapter';
type ShellHistoryStorage = {
read<T>(key: string): T | null;
write<T>(key: string, value: T): boolean;
};
export function loadSanitizedShellHistory(
storage: ShellHistoryStorage = localStorageAdapter,
storageKey = STORAGE_KEY_SHELL_HISTORY,
): ShellHistoryEntry[] | null {
const savedShellHistory = storage.read<ShellHistoryEntry[]>(storageKey);
if (!savedShellHistory) return null;
const cleanedShellHistory = sanitizeGlobalHistoryEntries(savedShellHistory);
if (cleanedShellHistory.length !== savedShellHistory.length) {
storage.write(storageKey, cleanedShellHistory);
}
return cleanedShellHistory;
}

View File

@@ -36,8 +36,9 @@ import {
STORAGE_KEY_TERM_SETTINGS,
} from "../../infrastructure/config/storageKeys";
import { localStorageAdapter } from "../../infrastructure/persistence/localStorageAdapter";
import { mergeGlobalHistoryOnAppend } from "../../domain/globalHistory";
import { mergeGlobalHistoryOnAppend, sanitizeGlobalHistoryEntries } from "../../domain/globalHistory";
import { getNextVaultOrder, normalizeVaultOrder } from "../../domain/vaultOrder";
import { loadSanitizedShellHistory } from "./shellHistoryPersistence";
import {
decryptGroupConfigs,
decryptHosts,
@@ -598,10 +599,10 @@ export const useVaultState = () => {
}
// Load shell history
const savedShellHistory = localStorageAdapter.read<ShellHistoryEntry[]>(
STORAGE_KEY_SHELL_HISTORY,
);
if (savedShellHistory) setShellHistory(savedShellHistory);
const savedShellHistory = loadSanitizedShellHistory();
if (savedShellHistory) {
setShellHistory(savedShellHistory);
}
// Load connection logs
const savedConnectionLogs = localStorageAdapter.read<ConnectionLog[]>(
@@ -729,7 +730,9 @@ export const useVaultState = () => {
}
if (key === STORAGE_KEY_SHELL_HISTORY) {
const next = safeParse<ShellHistoryEntry[]>(event.newValue) ?? [];
const next = sanitizeGlobalHistoryEntries(
safeParse<ShellHistoryEntry[]>(event.newValue) ?? [],
);
setShellHistory(next);
return;
}

View File

@@ -3,10 +3,13 @@ import assert from 'node:assert/strict';
import {
mergeGlobalHistoryOnAppend,
sanitizeGlobalHistoryEntries,
shouldRecordGlobalHistoryCommand,
toGlobalHistoryDisplayEntries,
} from './globalHistory.ts';
import { NETCATTY_AI_HISTORY_MARKER } from './remoteHistory.ts';
import { buildDockerExecShellCommand, buildDockerLogsCommand } from './systemManager/dockerShell.ts';
import { buildTmuxAttachCommand } from './systemManager/tmuxShell.ts';
import type { ShellHistoryEntry } from './models';
const baseEntry = (
@@ -30,6 +33,17 @@ test('shouldRecordGlobalHistoryCommand: rejects empty and AI marker commands', (
assert.equal(shouldRecordGlobalHistoryCommand('ls -la'), true);
});
test('shouldRecordGlobalHistoryCommand: rejects Netcatty managed Docker and tmux startup commands', () => {
assert.equal(shouldRecordGlobalHistoryCommand(buildDockerExecShellCommand('587abcdef123')), false);
assert.equal(shouldRecordGlobalHistoryCommand(buildDockerLogsCommand('587abcdef123')), false);
assert.equal(shouldRecordGlobalHistoryCommand(buildTmuxAttachCommand('my-session')), false);
assert.equal(shouldRecordGlobalHistoryCommand(buildTmuxAttachCommand('my-session', 2)), false);
assert.equal(shouldRecordGlobalHistoryCommand('docker ps -a'), true);
assert.equal(shouldRecordGlobalHistoryCommand('docker logs -f 587abcdef123'), true);
assert.equal(shouldRecordGlobalHistoryCommand('docker exec -it 587abcdef123 bash'), true);
assert.equal(shouldRecordGlobalHistoryCommand('tmux attach -t my-session'), true);
});
test('mergeGlobalHistoryOnAppend: trims and prepends a new command', () => {
const next = mergeGlobalHistoryOnAppend([], {
command: ' pwd ',
@@ -41,6 +55,19 @@ test('mergeGlobalHistoryOnAppend: trims and prepends a new command', () => {
assert.equal(next[0].command, 'pwd');
});
test('sanitizeGlobalHistoryEntries: removes persisted Netcatty managed startup commands', () => {
const entries = [
baseEntry({ id: 'a', command: buildDockerLogsCommand('587abcdef123') }),
baseEntry({ id: 'b', command: 'docker ps -a' }),
baseEntry({ id: 'c', command: buildTmuxAttachCommand('my-session') }),
];
const out = sanitizeGlobalHistoryEntries(entries);
assert.deepEqual(
out.map((entry) => entry.command),
['docker ps -a'],
);
});
test('mergeGlobalHistoryOnAppend: bumps timestamp for consecutive duplicate', () => {
const prev = [baseEntry({ id: 'a', command: 'ls', timestamp: 1000 })];
const next = mergeGlobalHistoryOnAppend(prev, {

View File

@@ -1,5 +1,8 @@
import type { ShellHistoryEntry } from './models';
import { isNetcattyAiHistoryCommand } from './remoteHistory';
import {
isNetcattyAiHistoryCommand,
isNetcattyManagedStartupHistoryCommand,
} from './remoteHistory';
const makeId = (): string => {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
@@ -13,9 +16,16 @@ export function shouldRecordGlobalHistoryCommand(command: string): boolean {
const cmd = command.trim();
if (!cmd) return false;
if (isNetcattyAiHistoryCommand(cmd)) return false;
if (isNetcattyManagedStartupHistoryCommand(cmd)) return false;
return true;
}
export function sanitizeGlobalHistoryEntries(
entries: ShellHistoryEntry[],
): ShellHistoryEntry[] {
return entries.filter((entry) => shouldRecordGlobalHistoryCommand(entry.command));
}
/**
* Append one command to global history: trim, drop noise, and de-dupe the most
* recent identical command by bumping its timestamp instead of adding a row.
@@ -61,7 +71,7 @@ export interface GlobalHistoryDisplayEntry {
export function toGlobalHistoryDisplayEntries(
entries: ShellHistoryEntry[],
): GlobalHistoryDisplayEntry[] {
return entries.map((entry) => ({
return sanitizeGlobalHistoryEntries(entries).map((entry) => ({
id: entry.id,
command: entry.command,
timestamp: entry.timestamp,

View File

@@ -8,7 +8,10 @@ import {
parseShellHistory,
mergeRemoteHistory,
isNetcattyAiHistoryCommand,
isNetcattyManagedStartupHistoryCommand,
} from './remoteHistory.ts';
import { buildDockerExecShellCommand, buildDockerLogsCommand } from './systemManager/dockerShell.ts';
import { buildTmuxAttachCommand } from './systemManager/tmuxShell.ts';
test('parseBashHistory: plain lines', () => {
const out = parseBashHistory(['ls -la', 'cd /tmp', 'echo hi'].join('\n'));
@@ -193,6 +196,17 @@ test('isNetcattyAiHistoryCommand: detects AI PTY marker lines', () => {
assert.equal(isNetcattyAiHistoryCommand('grep NCMCP log.txt'), false);
});
test('isNetcattyManagedStartupHistoryCommand: detects Docker and tmux terminal launch commands', () => {
assert.equal(isNetcattyManagedStartupHistoryCommand(buildDockerExecShellCommand('587abcdef123')), true);
assert.equal(isNetcattyManagedStartupHistoryCommand(buildDockerLogsCommand('587abcdef123')), true);
assert.equal(isNetcattyManagedStartupHistoryCommand(buildTmuxAttachCommand('my-session')), true);
assert.equal(isNetcattyManagedStartupHistoryCommand(buildTmuxAttachCommand('my-session', 2)), true);
assert.equal(isNetcattyManagedStartupHistoryCommand('docker ps -a'), false);
assert.equal(isNetcattyManagedStartupHistoryCommand('docker logs -f 587abcdef123'), false);
assert.equal(isNetcattyManagedStartupHistoryCommand('docker exec -it 587abcdef123 bash'), false);
assert.equal(isNetcattyManagedStartupHistoryCommand('tmux attach -t my-session'), false);
});
test('mergeRemoteHistory: drops Netcatty AI PTY history lines', () => {
const lists = [
parseBashHistory(
@@ -205,3 +219,44 @@ test('mergeRemoteHistory: drops Netcatty AI PTY history lines', () => {
['git status', 'ls -la'],
);
});
test('mergeRemoteHistory: drops Netcatty managed Docker and tmux startup lines', () => {
const lists = [
parseBashHistory(
[
'docker ps -a',
buildDockerLogsCommand('587abcdef123'),
buildTmuxAttachCommand('my-session'),
'history',
].join('\n'),
),
];
const merged = mergeRemoteHistory(lists);
assert.deepEqual(
merged.map((e) => e.command),
['history', 'docker ps -a'],
);
});
test('mergeRemoteHistory: drops Netcatty managed startup lines from zsh and fish history', () => {
const zsh = parseZshHistory(
[
': 1700000000:0;git status',
`: 1700000100:0;${buildDockerExecShellCommand('587abcdef123')}`,
].join('\n'),
);
const fish = parseFishHistory(
[
'- cmd: docker ps -a',
' when: 1700000200',
`- cmd: ${buildTmuxAttachCommand('my-session')}`,
' when: 1700000300',
].join('\n'),
);
const merged = mergeRemoteHistory([zsh, fish]);
assert.deepEqual(
merged.map((e) => e.command),
['docker ps -a', 'git status'],
);
});

View File

@@ -8,6 +8,14 @@ export function isNetcattyAiHistoryCommand(command: string): boolean {
return command.includes(NETCATTY_AI_HISTORY_MARKER);
}
const NETCATTY_MANAGED_STARTUP_COMMAND =
/^printf '\\033\[H\\033\[2J\\033\[3J';\s*exec\s+(?:docker\s+(?:exec|logs)\b|tmux\s+attach\b)/;
/** True when a shell history line came from a Netcatty-managed terminal launch. */
export function isNetcattyManagedStartupHistoryCommand(command: string): boolean {
return NETCATTY_MANAGED_STARTUP_COMMAND.test(command.trim());
}
const ZSH_EXTENDED_RECORD = /^: (\d+):\d+;([\s\S]*)$/;
// fish_history is a YAML subset: each record starts with `- cmd: <value>`,
// optionally followed by ` when: <epoch>` and a ` paths:` block.
@@ -215,6 +223,7 @@ export function mergeRemoteHistory(
const merged: RemoteHistoryEntry[] = [];
for (const { entry } of indexed) {
if (isNetcattyAiHistoryCommand(entry.command)) continue;
if (isNetcattyManagedStartupHistoryCommand(entry.command)) continue;
if (seen.has(entry.command)) continue;
seen.add(entry.command);
merged.push(entry);