one-code-extension 0.1.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +222 -177
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +128 -0
- package/docs/guide/configuration.md +173 -0
- package/docs/guide/differences-from-claude-code.md +152 -0
- package/docs/guide/doctor.md +88 -0
- package/docs/guide/hooks.md +111 -0
- package/docs/guide/installation.md +126 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +452 -0
- package/docs/guide/providers-and-models.md +196 -0
- package/docs/guide/reference.md +226 -0
- package/docs/guide/sessions-and-context.md +192 -0
- package/docs/guide/skills-plugins-and-mcp.md +178 -0
- package/docs/guide/subagents-and-workflows.md +195 -0
- package/docs/guide/tasks-and-background-work.md +113 -0
- package/docs/guide/terminal-interface.md +157 -0
- package/docs/guide/tools.md +177 -0
- package/docs/guide/troubleshooting.md +177 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-recoverability check for the containment pre-gate (pure core + thin git
|
|
3
|
+
* runner).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* Claude Code's auto mode trusts the whole project directory as the agent's
|
|
8
|
+
* sandbox: an in-project `rm` or `git reset --hard` is auto-approved with no
|
|
9
|
+
* classifier call, even when it destroys uncommitted or untracked work git can
|
|
10
|
+
* never restore (confirmed live — see docs/decisions/auto-mode.md). One Code
|
|
11
|
+
* keeps the cheap in-project fast path but refuses that specific risk: an
|
|
12
|
+
* in-project destructive action is auto-approved **only when git can put the
|
|
13
|
+
* bytes back** — the target is tracked and clean (whole-tree ops: the entire
|
|
14
|
+
* tree is clean). Anything else — untracked, dirty, not a git repo, or a git
|
|
15
|
+
* query we could not run — is `unknown`/`unrecoverable`, and the pre-gate
|
|
16
|
+
* escalates it to the classifier rather than destroying it silently.
|
|
17
|
+
*
|
|
18
|
+
* ## Contract
|
|
19
|
+
*
|
|
20
|
+
* Like the shell pre-gate, this may only ever *widen* what is provably safe.
|
|
21
|
+
* `recoverable` is returned only on positive proof (git says tracked+clean);
|
|
22
|
+
* every ambiguity — an error, a path git does not know, a non-repo — resolves to
|
|
23
|
+
* `unknown`, which the caller treats exactly like `unrecoverable`: classify.
|
|
24
|
+
* Being wrong toward `unknown` costs a classifier call; being wrong toward
|
|
25
|
+
* `recoverable` destroys the user's work, so uncertainty never lands there.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { execFileSync } from "node:child_process";
|
|
29
|
+
import { existsSync, lstatSync, readdirSync } from "node:fs";
|
|
30
|
+
import { basename, join, sep } from "node:path";
|
|
31
|
+
|
|
32
|
+
export type Recoverability = "recoverable" | "unrecoverable" | "unknown";
|
|
33
|
+
|
|
34
|
+
export interface RecoverabilityResult {
|
|
35
|
+
verdict: Recoverability;
|
|
36
|
+
/** One line naming why, for the decision log — never echoes file contents. */
|
|
37
|
+
reason: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What git reports about one destruction target. */
|
|
41
|
+
export interface TargetState {
|
|
42
|
+
/** Original token, for the reason string (never an expanded value). */
|
|
43
|
+
token: string;
|
|
44
|
+
/** `git ls-files` matched it — git has a committed/indexed copy to restore from. */
|
|
45
|
+
tracked: boolean;
|
|
46
|
+
/** `git status --porcelain --ignored` reported anything for it — uncommitted, untracked, or ignored content that a restore would not bring back. */
|
|
47
|
+
dirty: boolean;
|
|
48
|
+
/** True when the target is a directory (its reason names the content git would not restore). */
|
|
49
|
+
directory?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Judge a set of explicit destruction targets (`rm a b`, `truncate f`). Every
|
|
54
|
+
* target must be independently recoverable; the first that is not decides the
|
|
55
|
+
* whole action, since one command destroys them all.
|
|
56
|
+
*/
|
|
57
|
+
export function judgeTargets(targets: TargetState[]): RecoverabilityResult {
|
|
58
|
+
if (targets.length === 0) return { verdict: "unknown", reason: "no destruction target could be identified" };
|
|
59
|
+
for (const target of targets) {
|
|
60
|
+
if (!target.tracked) {
|
|
61
|
+
return { verdict: "unrecoverable", reason: `${target.token} is not tracked by git, so deleting it is not reversible` };
|
|
62
|
+
}
|
|
63
|
+
if (target.dirty) {
|
|
64
|
+
return {
|
|
65
|
+
verdict: "unrecoverable",
|
|
66
|
+
reason: target.directory
|
|
67
|
+
? `${target.token} is a directory holding uncommitted, untracked, or ignored files that a git restore would not bring back`
|
|
68
|
+
: `${target.token} has uncommitted changes that a git restore would not bring back`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { verdict: "recoverable", reason: "every target is tracked by git and clean, so git can restore it" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Judge a whole-tree destructive op. Recoverable only when the entire working
|
|
77
|
+
* tree is clean: a clean tree's state is committed (reachable again via
|
|
78
|
+
* reflog/checkout), while any modified, staged, or untracked entry is content the
|
|
79
|
+
* op would erase for good. This judge is generic over any tree-wide reset, but at
|
|
80
|
+
* present only `git reset --hard` is routed here — `git clean -fd` / `git checkout
|
|
81
|
+
* .` are escalated directly by shell-analysis (isWholeTreeGitReset), so they reach
|
|
82
|
+
* the classifier rather than this fast path.
|
|
83
|
+
*/
|
|
84
|
+
export function judgeWholeTree(isRepo: boolean, statusPorcelain: string): RecoverabilityResult {
|
|
85
|
+
if (!isRepo) return { verdict: "unrecoverable", reason: "not a git repository, so a tree-wide reset cannot be undone" };
|
|
86
|
+
return statusPorcelain.trim().length === 0
|
|
87
|
+
? { verdict: "recoverable", reason: "the working tree is clean, so git can restore it" }
|
|
88
|
+
: { verdict: "unrecoverable", reason: "the working tree has uncommitted or untracked changes a reset would destroy" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Combine several sub-verdicts: the whole action is only as recoverable as its least-recoverable part. */
|
|
92
|
+
export function combine(results: RecoverabilityResult[]): RecoverabilityResult {
|
|
93
|
+
if (results.length === 0) return { verdict: "unknown", reason: "nothing to check" };
|
|
94
|
+
const worst = results.find((r) => r.verdict === "unrecoverable") ?? results.find((r) => r.verdict === "unknown");
|
|
95
|
+
return worst ?? results[0];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The path is `.git`, sits inside a `.git`, or is a directory with a `.git` entry (repo root, nested repo, worktree). */
|
|
99
|
+
function isOrHoldsGitDir(path: string): boolean {
|
|
100
|
+
if (basename(path) === ".git") return true;
|
|
101
|
+
if (path.split(sep).includes(".git")) return true;
|
|
102
|
+
try {
|
|
103
|
+
return lstatSync(path).isDirectory() && existsSync(join(path, ".git"));
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether any entry named `.git` exists anywhere under `dir`. Bounded by
|
|
111
|
+
* `budget` directory entries; returns undefined when the budget runs out (the
|
|
112
|
+
* caller escalates). Symlinks are not followed.
|
|
113
|
+
*/
|
|
114
|
+
function holdsNestedGitDir(dir: string, budget: number): boolean | undefined {
|
|
115
|
+
const stack = [dir];
|
|
116
|
+
let seen = 0;
|
|
117
|
+
while (stack.length > 0) {
|
|
118
|
+
const current = stack.pop() as string;
|
|
119
|
+
let entries;
|
|
120
|
+
try {
|
|
121
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
122
|
+
} catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (++seen > budget) return undefined;
|
|
127
|
+
if (entry.name === ".git") return true;
|
|
128
|
+
if (entry.isDirectory()) stack.push(join(current, entry.name));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A thin, defensive git call: any failure (not a repo, git missing, timeout) becomes `undefined`. */
|
|
135
|
+
function git(cwd: string, args: string[]): { ok: boolean; stdout: string } | undefined {
|
|
136
|
+
try {
|
|
137
|
+
const stdout = execFileSync("git", args, { cwd, encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "ignore"] });
|
|
138
|
+
return { ok: true, stdout };
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// A non-zero exit still carries stdout (e.g. ls-files --error-unmatch), which
|
|
141
|
+
// the caller may want; distinguish "ran and said no" from "could not run".
|
|
142
|
+
const status = (error as { status?: number }).status;
|
|
143
|
+
if (typeof status === "number") return { ok: false, stdout: String((error as { stdout?: unknown }).stdout ?? "") };
|
|
144
|
+
return undefined; // git not found / timed out / killed — genuinely unknown
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface Destruction {
|
|
149
|
+
/** Resolved absolute paths the command destroys (already proven in-project by the pre-gate). */
|
|
150
|
+
targets: string[];
|
|
151
|
+
/** True for reset --hard / clean / checkout . — the target is the whole working tree, not named paths. */
|
|
152
|
+
wholeTree: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Run the git queries and judge. `cwd` is the command's effective directory.
|
|
157
|
+
* Returns `unknown` on any git failure so the caller escalates rather than
|
|
158
|
+
* trusting an answer we could not compute.
|
|
159
|
+
*/
|
|
160
|
+
export function checkRecoverability(cwd: string, destruction: Destruction): RecoverabilityResult {
|
|
161
|
+
const repoProbe = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
162
|
+
const isRepo = repoProbe?.ok === true && repoProbe.stdout.trim() === "true";
|
|
163
|
+
|
|
164
|
+
if (destruction.wholeTree) {
|
|
165
|
+
if (!isRepo) return judgeWholeTree(false, "");
|
|
166
|
+
const status = git(cwd, ["status", "--porcelain"]);
|
|
167
|
+
if (!status) return { verdict: "unknown", reason: "could not read git status to judge a tree-wide reset" };
|
|
168
|
+
return judgeWholeTree(true, status.stdout);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (destruction.targets.length === 0) return { verdict: "unknown", reason: "no destruction target could be identified" };
|
|
172
|
+
if (!isRepo) return { verdict: "unrecoverable", reason: "not a git repository, so deletion is not reversible" };
|
|
173
|
+
|
|
174
|
+
const states: TargetState[] = [];
|
|
175
|
+
for (const path of destruction.targets) {
|
|
176
|
+
// A target that does not exist has nothing to lose: deleting it is a no-op,
|
|
177
|
+
// truncating it creates a fresh empty file. Either way there is no
|
|
178
|
+
// pre-existing content a restore would need to bring back. Use lstat, not
|
|
179
|
+
// existsSync: existsSync follows a symlink to its target, so a tracked-but-
|
|
180
|
+
// dirty *broken* symlink (its target missing) would read as "nothing to
|
|
181
|
+
// lose" and be wrongly cleared as recoverable — the link entry itself is
|
|
182
|
+
// real, uncommitted content, so it must be queried against git.
|
|
183
|
+
let entryExists = true;
|
|
184
|
+
try {
|
|
185
|
+
lstatSync(path);
|
|
186
|
+
} catch {
|
|
187
|
+
entryExists = false;
|
|
188
|
+
}
|
|
189
|
+
if (!entryExists) continue;
|
|
190
|
+
// The repository itself is never recoverable from itself: `rm -rf .` at the
|
|
191
|
+
// root passes every tracked/clean test (git ls-files matches any dir with
|
|
192
|
+
// tracked files) yet deletes `.git`. Same for a path inside `.git` and for a
|
|
193
|
+
// directory holding a nested repository or worktree link.
|
|
194
|
+
if (isOrHoldsGitDir(path)) {
|
|
195
|
+
return { verdict: "unrecoverable", reason: `${path} is or contains a .git directory, so deleting it destroys the history that would restore it` };
|
|
196
|
+
}
|
|
197
|
+
const isDirectory = (() => {
|
|
198
|
+
try {
|
|
199
|
+
return lstatSync(path).isDirectory();
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
})();
|
|
204
|
+
// `--ignored`: porcelain omits ignored files by default, so a directory of
|
|
205
|
+
// build output or a `.env` read as "clean" while `rm -rf` erased it for good.
|
|
206
|
+
const tracked = git(cwd, ["ls-files", "--error-unmatch", "--", path]);
|
|
207
|
+
const status = git(cwd, ["status", "--porcelain", "--ignored", "--", path]);
|
|
208
|
+
if (tracked === undefined || status === undefined) {
|
|
209
|
+
return { verdict: "unknown", reason: "could not query git for a deletion target" };
|
|
210
|
+
}
|
|
211
|
+
const dirty = status.stdout.trim().length > 0;
|
|
212
|
+
// A clean, tracked directory can still hold a nested repository deeper down
|
|
213
|
+
// (git status never lists a `.git`, so it reads as clean). Only scanned once
|
|
214
|
+
// the cheap checks passed; a huge tree exhausts the budget and stays unknown.
|
|
215
|
+
if (isDirectory && tracked.ok && !dirty) {
|
|
216
|
+
const nested = holdsNestedGitDir(path, 5_000);
|
|
217
|
+
if (nested === undefined) {
|
|
218
|
+
return { verdict: "unknown", reason: `${path} is too large to scan for nested repositories` };
|
|
219
|
+
}
|
|
220
|
+
if (nested) {
|
|
221
|
+
return { verdict: "unrecoverable", reason: `${path} contains a nested .git directory whose history git cannot restore` };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
states.push({ token: path, tracked: tracked.ok, dirty, directory: isDirectory });
|
|
225
|
+
}
|
|
226
|
+
// Every target was a non-existent path (pure creation / no-op) — nothing to lose.
|
|
227
|
+
if (states.length === 0) return { verdict: "recoverable", reason: "no existing content would be destroyed" };
|
|
228
|
+
return judgeTargets(states);
|
|
229
|
+
}
|
|
@@ -1,96 +1,113 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Grounding the stage-2 verdict against CC's rule names (pure).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Claude Code's stage-2 classifier emits `<category>Exact BLOCK Rule Name</category>`
|
|
5
|
+
* when an action matches a BLOCK rule. CC itself discards that and shows a fixed
|
|
6
|
+
* "Blocked by classifier"; we instead validate the category against an index of
|
|
7
|
+
* the rule names actually present in the embedded ruleset, and derive the tier
|
|
8
|
+
* (hard vs soft) from which section the name was found in — never from a field
|
|
9
|
+
* the model chose freely.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* Claude Code went further and stopped showing a reason at all (v2.1.208 shows a
|
|
18
|
-
* fixed "Blocked by classifier"), having shipped written explanations first and
|
|
19
|
-
* moved off them. Keeping the explanation is worth it — it is how a user knows
|
|
20
|
-
* which rule or environment entry to change — but only if it cannot assert
|
|
21
|
-
* things that are not so.
|
|
11
|
+
* A block citing a category that does not exist is still a block, but tagged
|
|
12
|
+
* `unmatched`, so a hallucinated name cannot borrow a real HARD rule's authority.
|
|
13
|
+
* The rule's own name is what the user sees; the model's wording is kept as
|
|
14
|
+
* commentary that is never mistaken for fact. This is decision 1 in
|
|
15
|
+
* docs/decisions/auto-mode.md, re-expressed against CC's category scheme in
|
|
16
|
+
* place of our old H1/S4/A2 ids.
|
|
22
17
|
*/
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export type RuleTier = "hard_deny" | "soft_deny" | "allow";
|
|
19
|
+
export type BlockTier = "hard_deny" | "soft_deny";
|
|
27
20
|
|
|
28
21
|
export interface IndexedRule {
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
tier:
|
|
32
|
-
/** The rule's full text, as configured. */
|
|
33
|
-
text: string;
|
|
34
|
-
/** Short human-readable name, for a one-line denial listing. */
|
|
35
|
-
label: string;
|
|
22
|
+
/** The rule's exact name as it appears in the ruleset (e.g. "Git Destructive"). */
|
|
23
|
+
name: string;
|
|
24
|
+
tier: BlockTier;
|
|
36
25
|
}
|
|
37
26
|
|
|
38
|
-
|
|
27
|
+
export interface RuleIndex {
|
|
28
|
+
rules: IndexedRule[];
|
|
29
|
+
/** Keyed by normalized name, for case/whitespace-insensitive lookup. */
|
|
30
|
+
byName: Map<string, IndexedRule>;
|
|
31
|
+
}
|
|
39
32
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
},
|
|
54
|
-
unclear: {
|
|
55
|
-
reason: "What this action would do could not be determined from the call, so it was not allowed.",
|
|
56
|
-
tier: "soft_deny",
|
|
57
|
-
},
|
|
58
|
-
};
|
|
33
|
+
/** Section headings in CC's ruleset that bound the block-rule lists. */
|
|
34
|
+
const HARD_HEADING = "## HARD BLOCK";
|
|
35
|
+
const SOFT_HEADING = "## SOFT BLOCK";
|
|
36
|
+
const ALLOW_HEADING = "## ALLOW (exceptions)";
|
|
37
|
+
|
|
38
|
+
/** Compare names ignoring case, surrounding punctuation, and whitespace runs. */
|
|
39
|
+
export function normalizeName(value: string): string {
|
|
40
|
+
return value
|
|
41
|
+
.trim()
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.replace(/\s+/g, " ")
|
|
44
|
+
.replace(/[.,:;]+$/, "");
|
|
45
|
+
}
|
|
59
46
|
|
|
60
47
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
48
|
+
* The rule name at the head of a top-level bullet line, or undefined if the line
|
|
49
|
+
* is not a rule entry. A rule line looks like `- Name [named+specifics …]: text`
|
|
50
|
+
* or `- Name: text`; the name is what precedes the first ` [` qualifier or `:`.
|
|
51
|
+
* Indented lines (sub-bullets) and non-bullet prose are ignored.
|
|
64
52
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
53
|
+
function ruleNameFromLine(line: string): string | undefined {
|
|
54
|
+
if (!line.startsWith("- ")) return undefined; // top-level bullets only
|
|
55
|
+
const body = line.slice(2);
|
|
56
|
+
// Cut at the first " [" (the [named+specifics …] qualifier) or the first ":".
|
|
57
|
+
const bracket = body.indexOf(" [");
|
|
58
|
+
const colon = body.indexOf(":");
|
|
59
|
+
let end = body.length;
|
|
60
|
+
if (bracket >= 0) end = Math.min(end, bracket);
|
|
61
|
+
if (colon >= 0) end = Math.min(end, colon);
|
|
62
|
+
const name = body.slice(0, end).replace(/\*/g, "").trim();
|
|
63
|
+
return name.length > 0 ? name : undefined;
|
|
71
64
|
}
|
|
72
65
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
/** The text between two markers (exclusive), or "" if the bounds are missing. */
|
|
67
|
+
function sliceSection(text: string, from: string, to: string): string {
|
|
68
|
+
const start = text.indexOf(from);
|
|
69
|
+
if (start < 0) return "";
|
|
70
|
+
const end = text.indexOf(to, start + from.length);
|
|
71
|
+
return text.slice(start + from.length, end < 0 ? text.length : end);
|
|
76
72
|
}
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
74
|
+
function collectRules(section: string, tier: BlockTier, out: IndexedRule[], byName: Map<string, IndexedRule>): void {
|
|
75
|
+
for (const line of section.split("\n")) {
|
|
76
|
+
const name = ruleNameFromLine(line);
|
|
77
|
+
if (!name) continue;
|
|
78
|
+
const nameKey = normalizeName(name);
|
|
79
|
+
if (byName.has(nameKey)) continue; // a name appearing in two sections keeps its first (HARD) tier
|
|
80
|
+
const rule: IndexedRule = { name, tier };
|
|
81
|
+
out.push(rule);
|
|
82
|
+
byName.set(nameKey, rule);
|
|
83
|
+
// Also key the FULL line text (when it differs), so a verdict quoting the
|
|
84
|
+
// whole rule line still grounds. Built-in and user-appended lines get this
|
|
85
|
+
// uniformly — user extras especially need it, since they have no short
|
|
86
|
+
// `Name:` head and may contain `*`s that the derived name strips.
|
|
87
|
+
const fullKey = normalizeName(line.slice(2));
|
|
88
|
+
if (fullKey && fullKey !== nameKey && !byName.has(fullKey)) byName.set(fullKey, rule);
|
|
87
89
|
}
|
|
88
|
-
return { rules, byId: new Map(rules.map((rule) => [rule.id, rule])) };
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Parse the block-rule names out of the embedded ruleset (user rule extras
|
|
94
|
+
* included — buildRuleset has already appended them into these sections, so
|
|
95
|
+
* they are indexed by the same scan as the built-ins). HARD BLOCK names win a
|
|
96
|
+
* tie over SOFT (a HARD rule must never be downgraded by a same-named soft
|
|
97
|
+
* one). ALLOW names are not block categories; they are intentionally not
|
|
98
|
+
* indexed, so a block citing an allow name lands as `unmatched` rather than a
|
|
99
|
+
* real block.
|
|
100
|
+
*/
|
|
101
|
+
export function buildCategoryIndex(ruleset: string): RuleIndex {
|
|
102
|
+
const rules: IndexedRule[] = [];
|
|
103
|
+
const byName = new Map<string, IndexedRule>();
|
|
104
|
+
collectRules(sliceSection(ruleset, HARD_HEADING, SOFT_HEADING), "hard_deny", rules, byName);
|
|
105
|
+
collectRules(sliceSection(ruleset, SOFT_HEADING, ALLOW_HEADING), "soft_deny", rules, byName);
|
|
106
|
+
return { rules, byName };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Look up a category the classifier emitted. undefined ⇒ not a known BLOCK rule. */
|
|
110
|
+
export function groundCategory(index: RuleIndex, category: string | undefined): IndexedRule | undefined {
|
|
111
|
+
if (!category) return undefined;
|
|
112
|
+
return index.byName.get(normalizeName(category));
|
|
96
113
|
}
|
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
* fires on routine work teaches the user to approve without reading.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { join } from "node:path";
|
|
26
25
|
import { analyzeShellCommand } from "./shell-analysis.ts";
|
|
27
26
|
import { autoModeSettingsPaths } from "./config.ts";
|
|
27
|
+
import { oneCodeProjectSettingsPath } from "../lib/one-code-settings.ts";
|
|
28
|
+
import { claudeJsonPath } from "../lib/paths.ts";
|
|
28
29
|
import { isWritingTool, resolveForContainment, toAbsolute } from "./paths.ts";
|
|
29
30
|
|
|
30
31
|
/** Case-fold the same way resolveForContainment's output is folded. */
|
|
@@ -41,22 +42,43 @@ function fold(path: string): string {
|
|
|
41
42
|
*/
|
|
42
43
|
const SETTINGS_TAIL = /\/\.claude\/settings(\.local)?\.json$/;
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
/**
|
|
46
|
+
* One Code's own settings files — the global `~/.onecode/settings.json` and the
|
|
47
|
+
* per-repo `~/.onecode/projects/<slug>/settings.json`, wherever `.onecode`
|
|
48
|
+
* lives. Both now carry `permissions.allow` rules that `loadPermissionSettings`
|
|
49
|
+
* reads and that bypass the classifier, so a write to either is a gate-control
|
|
50
|
+
* write and must hit this floor rather than the auto-mode classifier (the global
|
|
51
|
+
* file is also reached via `autoModeSettingsPaths`; this covers the per-repo one,
|
|
52
|
+
* which that list never includes, and both under any checkout).
|
|
53
|
+
*/
|
|
54
|
+
const ONECODE_SETTINGS_TAIL = /\/\.onecode\/(projects\/[^/]+\/)?settings\.json$/;
|
|
55
|
+
|
|
56
|
+
function safetyControlFiles(home: string, oneCodeProjectSettings?: string): string[] {
|
|
45
57
|
return [
|
|
46
|
-
// autoMode + user permission rules, and the managed-settings paths
|
|
58
|
+
// autoMode + user permission rules, and the managed-settings paths (the
|
|
59
|
+
// global ~/.onecode/settings.json here honours ONECODE_STATE_DIR).
|
|
47
60
|
...autoModeSettingsPaths(home),
|
|
48
|
-
// Claude Code's global state file also carries permission configuration
|
|
49
|
-
|
|
61
|
+
// Claude Code's global state file also carries permission configuration;
|
|
62
|
+
// claudeJsonPath honours CLAUDE_CONFIG_DIR so a relocated file is still caught.
|
|
63
|
+
claudeJsonPath(home),
|
|
64
|
+
// The per-repo One Code settings file also carries permission rules. The
|
|
65
|
+
// tail regex catches it under a literal `.onecode`; this catches it when
|
|
66
|
+
// ONECODE_STATE_DIR relocated the state root (review L2).
|
|
67
|
+
...(oneCodeProjectSettings ? [oneCodeProjectSettings] : []),
|
|
50
68
|
];
|
|
51
69
|
}
|
|
52
70
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Whether a *resolved* path (resolveForContainment output) is a gate control.
|
|
73
|
+
* `oneCodeProjectSettings` is the current repo's One Code settings file, resolved
|
|
74
|
+
* by the caller (once per session) so this need not re-walk for the project root.
|
|
75
|
+
*/
|
|
76
|
+
export function isSafetyControlTarget(resolved: string, home: string, oneCodeProjectSettings?: string): boolean {
|
|
55
77
|
const target = fold(resolved);
|
|
56
|
-
if (SETTINGS_TAIL.test(target)) return true;
|
|
78
|
+
if (SETTINGS_TAIL.test(target) || ONECODE_SETTINGS_TAIL.test(target)) return true;
|
|
57
79
|
// The control files go through the same resolution as the write target, or
|
|
58
80
|
// the two sides can disagree about the same file (macOS /var → /private/var).
|
|
59
|
-
return safetyControlFiles(home).some((file) => {
|
|
81
|
+
return safetyControlFiles(home, oneCodeProjectSettings).some((file) => {
|
|
60
82
|
const control = resolveForContainment(file) ?? fold(file);
|
|
61
83
|
return control === target || fold(file) === target;
|
|
62
84
|
});
|
|
@@ -68,6 +90,13 @@ export interface FloorInput {
|
|
|
68
90
|
input: Record<string, unknown>;
|
|
69
91
|
cwd: string;
|
|
70
92
|
home: string;
|
|
93
|
+
/**
|
|
94
|
+
* The current repo's One Code settings file, resolved once per session by the
|
|
95
|
+
* caller. When omitted it is derived from `cwd` (a filesystem walk) — fine for
|
|
96
|
+
* tests, but the per-tool-call permission path passes it to avoid the walk
|
|
97
|
+
* (distribution review 2026-09-09, L2 / efficiency pass).
|
|
98
|
+
*/
|
|
99
|
+
oneCodeProjectSettings?: string;
|
|
71
100
|
}
|
|
72
101
|
|
|
73
102
|
const REASON = (token: string) =>
|
|
@@ -79,12 +108,14 @@ const REASON = (token: string) =>
|
|
|
79
108
|
* files included), so linking a settings file elsewhere and writing the link
|
|
80
109
|
* does not slip past.
|
|
81
110
|
*/
|
|
82
|
-
export function safetyControlWrite({ toolName, input, cwd, home }: FloorInput): string | undefined {
|
|
111
|
+
export function safetyControlWrite({ toolName, input, cwd, home, oneCodeProjectSettings }: FloorInput): string | undefined {
|
|
112
|
+
const perRepoSettings = oneCodeProjectSettings ?? oneCodeProjectSettingsPath(cwd, home);
|
|
113
|
+
|
|
83
114
|
if (isWritingTool(toolName)) {
|
|
84
115
|
const raw = input.path ?? input.file_path ?? input.notebook_path;
|
|
85
116
|
if (typeof raw !== "string" || raw.length === 0) return undefined;
|
|
86
117
|
const resolved = resolveForContainment(toAbsolute(cwd, raw, home));
|
|
87
|
-
return resolved && isSafetyControlTarget(resolved, home) ? REASON(raw) : undefined;
|
|
118
|
+
return resolved && isSafetyControlTarget(resolved, home, perRepoSettings) ? REASON(raw) : undefined;
|
|
88
119
|
}
|
|
89
120
|
|
|
90
121
|
if (toolName === "bash") {
|
|
@@ -92,7 +123,7 @@ export function safetyControlWrite({ toolName, input, cwd, home }: FloorInput):
|
|
|
92
123
|
if (!command) return undefined;
|
|
93
124
|
const evidence = analyzeShellCommand({ command, cwd, home });
|
|
94
125
|
for (const write of evidence.writes) {
|
|
95
|
-
if (write.resolved && isSafetyControlTarget(write.resolved, home)) return REASON(write.token);
|
|
126
|
+
if (write.resolved && isSafetyControlTarget(write.resolved, home, perRepoSettings)) return REASON(write.token);
|
|
96
127
|
}
|
|
97
128
|
}
|
|
98
129
|
|
|
@@ -36,9 +36,20 @@ const SENSITIVE_DIR_PAIRS = [
|
|
|
36
36
|
[".config", "gcloud"],
|
|
37
37
|
[".config", "gh"],
|
|
38
38
|
[".local", "share"],
|
|
39
|
+
[".huggingface", "token"],
|
|
40
|
+
["library", "keychains"], // macOS keychain databases
|
|
39
41
|
] as const;
|
|
40
42
|
|
|
41
43
|
const SENSITIVE_BASENAMES = new Set([
|
|
44
|
+
// The harness's own credential stores: pi's / One Code's `agent/auth.json`
|
|
45
|
+
// and Claude Code's `~/.claude/.credentials.json` (PERMISSIONS-REVIEW-2026-09-05
|
|
46
|
+
// H2 — the classifier's own API key was readable unclassified).
|
|
47
|
+
"auth.json",
|
|
48
|
+
".credentials.json",
|
|
49
|
+
"credentials.json",
|
|
50
|
+
"credentials.toml", // cargo
|
|
51
|
+
".vault-token",
|
|
52
|
+
".gitconfig", // credential helpers, embedded tokens
|
|
42
53
|
".bash_history",
|
|
43
54
|
".bash_profile",
|
|
44
55
|
".bashrc",
|