residoo 0.15.0 → 0.17.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "residoo",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Find secrets leaking through your AI coding agent's session history. Zero network calls in the scan path, zero dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "CloudRoam (https://cloudroam.io)",
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The canonical BIP-39 English wordlist (2048 words), verbatim from the
5
+ * spec's own reference file: github.com/bitcoin/bips, bip-0039/english.txt
6
+ * (fetched directly, not retyped from memory -- transcription errors here
7
+ * would silently break every checksum computation in src/pii.js). Order
8
+ * matters: word N's position in this array IS its 11-bit index per the
9
+ * spec, so this list must never be re-sorted or edited.
10
+ */
11
+ const BIP39_ENGLISH_WORDLIST = [
12
+ "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract",
13
+ "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid",
14
+ "acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual",
15
+ "adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance",
16
+ "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent",
17
+ "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album",
18
+ "alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone",
19
+ "alpha", "already", "also", "alter", "always", "amateur", "amazing", "among",
20
+ "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry",
21
+ "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique",
22
+ "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april",
23
+ "arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor",
24
+ "army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact",
25
+ "artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume",
26
+ "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction",
27
+ "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado",
28
+ "avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis",
29
+ "baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball",
30
+ "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base",
31
+ "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become",
32
+ "beef", "before", "begin", "behave", "behind", "believe", "below", "belt",
33
+ "bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle",
34
+ "bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black",
35
+ "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood",
36
+ "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body",
37
+ "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring",
38
+ "borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain",
39
+ "brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief",
40
+ "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother",
41
+ "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb",
42
+ "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus",
43
+ "business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable",
44
+ "cactus", "cage", "cake", "call", "calm", "camera", "camp", "can",
45
+ "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable",
46
+ "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry",
47
+ "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog",
48
+ "catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling",
49
+ "celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk",
50
+ "champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap",
51
+ "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child",
52
+ "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar",
53
+ "cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify",
54
+ "claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff",
55
+ "climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud",
56
+ "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut",
57
+ "code", "coffee", "coil", "coin", "collect", "color", "column", "combine",
58
+ "come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm",
59
+ "congress", "connect", "consider", "control", "convince", "cook", "cool", "copper",
60
+ "copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch",
61
+ "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle",
62
+ "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream",
63
+ "credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop",
64
+ "cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch",
65
+ "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious",
66
+ "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad",
67
+ "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn",
68
+ "day", "deal", "debate", "debris", "decade", "december", "decide", "decline",
69
+ "decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay",
70
+ "deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend",
71
+ "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk",
72
+ "despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram",
73
+ "dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital",
74
+ "dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover",
75
+ "disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide",
76
+ "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain",
77
+ "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft",
78
+ "dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill",
79
+ "drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb",
80
+ "dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager",
81
+ "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo",
82
+ "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight",
83
+ "either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator",
84
+ "elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ",
85
+ "empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy",
86
+ "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough",
87
+ "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode",
88
+ "equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt",
89
+ "escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil",
90
+ "evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude",
91
+ "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit",
92
+ "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend",
93
+ "extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint",
94
+ "faith", "fall", "false", "fame", "family", "famous", "fan", "fancy",
95
+ "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault",
96
+ "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female",
97
+ "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field",
98
+ "figure", "file", "film", "filter", "final", "find", "fine", "finger",
99
+ "finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness",
100
+ "fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight",
101
+ "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly",
102
+ "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot",
103
+ "force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil",
104
+ "foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend",
105
+ "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel",
106
+ "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy",
107
+ "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment",
108
+ "gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius",
109
+ "genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle",
110
+ "ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass",
111
+ "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue",
112
+ "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip",
113
+ "govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass",
114
+ "gravity", "great", "green", "grid", "grief", "grit", "grocery", "group",
115
+ "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun",
116
+ "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy",
117
+ "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard",
118
+ "head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet",
119
+ "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip",
120
+ "hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow",
121
+ "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital",
122
+ "host", "hotel", "hour", "hover", "hub", "huge", "human", "humble",
123
+ "humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband",
124
+ "hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill",
125
+ "illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose",
126
+ "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate",
127
+ "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial",
128
+ "inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane",
129
+ "insect", "inside", "inspire", "install", "intact", "interest", "into", "invest",
130
+ "invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory",
131
+ "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel",
132
+ "job", "join", "joke", "journey", "joy", "judge", "juice", "jump",
133
+ "jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup",
134
+ "key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit",
135
+ "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know",
136
+ "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language",
137
+ "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law",
138
+ "lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave",
139
+ "lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend",
140
+ "length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty",
141
+ "library", "license", "life", "lift", "light", "like", "limb", "limit",
142
+ "link", "lion", "liquid", "list", "little", "live", "lizard", "load",
143
+ "loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop",
144
+ "lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber",
145
+ "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet",
146
+ "maid", "mail", "main", "major", "make", "mammal", "man", "manage",
147
+ "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin",
148
+ "marine", "market", "marriage", "mask", "mass", "master", "match", "material",
149
+ "math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure",
150
+ "meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory",
151
+ "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message",
152
+ "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind",
153
+ "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake",
154
+ "mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment",
155
+ "monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning",
156
+ "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie",
157
+ "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music",
158
+ "must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin",
159
+ "narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative",
160
+ "neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral",
161
+ "never", "news", "next", "nice", "night", "noble", "noise", "nominee",
162
+ "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice",
163
+ "novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey",
164
+ "object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean",
165
+ "october", "odor", "off", "offer", "office", "often", "oil", "okay",
166
+ "old", "olive", "olympic", "omit", "once", "one", "onion", "online",
167
+ "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit",
168
+ "orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich",
169
+ "other", "outdoor", "outer", "output", "outside", "oval", "oven", "over",
170
+ "own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page",
171
+ "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper",
172
+ "parade", "parent", "park", "parrot", "party", "pass", "patch", "path",
173
+ "patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut",
174
+ "pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper",
175
+ "perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical",
176
+ "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot",
177
+ "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet",
178
+ "plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge",
179
+ "poem", "poet", "point", "polar", "pole", "police", "pond", "pony",
180
+ "pool", "popular", "portion", "position", "possible", "post", "potato", "pottery",
181
+ "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare",
182
+ "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority",
183
+ "prison", "private", "prize", "problem", "process", "produce", "profit", "program",
184
+ "project", "promote", "proof", "property", "prosper", "protect", "proud", "provide",
185
+ "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil",
186
+ "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle",
187
+ "pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz",
188
+ "quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail",
189
+ "rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid",
190
+ "rare", "rate", "rather", "raven", "raw", "razor", "ready", "real",
191
+ "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle",
192
+ "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject",
193
+ "relax", "release", "relief", "rely", "remain", "remember", "remind", "remove",
194
+ "render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report",
195
+ "require", "rescue", "resemble", "resist", "resource", "response", "result", "retire",
196
+ "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib",
197
+ "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid",
198
+ "ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road",
199
+ "roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room",
200
+ "rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude",
201
+ "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness",
202
+ "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same",
203
+ "sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say",
204
+ "scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science",
205
+ "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea",
206
+ "search", "season", "seat", "second", "secret", "section", "security", "seed",
207
+ "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence",
208
+ "series", "service", "session", "settle", "setup", "seven", "shadow", "shaft",
209
+ "shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine",
210
+ "ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder",
211
+ "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side",
212
+ "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar",
213
+ "simple", "since", "sing", "siren", "sister", "situate", "six", "size",
214
+ "skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab",
215
+ "slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan",
216
+ "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth",
217
+ "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social",
218
+ "sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve",
219
+ "someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup",
220
+ "source", "south", "space", "spare", "spatial", "spawn", "speak", "special",
221
+ "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin",
222
+ "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray",
223
+ "spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium",
224
+ "staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay",
225
+ "steak", "steel", "stem", "step", "stereo", "stick", "still", "sting",
226
+ "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street",
227
+ "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject",
228
+ "submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest",
229
+ "suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme",
230
+ "sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain",
231
+ "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim",
232
+ "swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table",
233
+ "tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target",
234
+ "task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten",
235
+ "tenant", "tennis", "tent", "term", "test", "text", "thank", "that",
236
+ "theme", "then", "theory", "there", "they", "thing", "this", "thought",
237
+ "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger",
238
+ "tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title",
239
+ "toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token",
240
+ "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top",
241
+ "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist",
242
+ "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic",
243
+ "train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree",
244
+ "trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy",
245
+ "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try",
246
+ "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle",
247
+ "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical",
248
+ "ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo",
249
+ "unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown",
250
+ "unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon",
251
+ "upper", "upset", "urban", "urge", "usage", "use", "used", "useful",
252
+ "useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley",
253
+ "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle",
254
+ "velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very",
255
+ "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view",
256
+ "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual",
257
+ "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote",
258
+ "voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want",
259
+ "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave",
260
+ "way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding",
261
+ "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat",
262
+ "wheel", "when", "where", "whip", "whisper", "wide", "width", "wife",
263
+ "wild", "will", "win", "window", "wine", "wing", "wink", "winner",
264
+ "winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman",
265
+ "wonder", "wood", "wool", "word", "work", "world", "worry", "worth",
266
+ "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year",
267
+ "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo",
268
+ ];
269
+
270
+ module.exports = { BIP39_ENGLISH_WORDLIST };
package/src/cli.js CHANGED
@@ -150,13 +150,19 @@ Scan options:
150
150
  similar characters (0/O, Y/*) can be misread,
151
151
  which breaks an exact-format match, so this is
152
152
  best-effort additional coverage, not a guarantee.
153
- --include-pii also scan for PII: US Social Security Numbers
154
- (dashed format only), credit card numbers
155
- (Luhn-validated), and IBANs (checksum-validated).
156
- A different RISK CATEGORY, not a lower confidence
157
- bar -- residoo is deliberately credentials-only
158
- by default. Deliberately excludes bare email/
159
- phone (too common in ordinary text to meet this
153
+ --include-pii also scan for PII and adjacent secrets: US Social
154
+ Security Numbers (dashed format only), credit
155
+ card numbers (Luhn-validated), IBANs (checksum-
156
+ validated), and crypto wallet seed phrases
157
+ (BIP-39, SHA-256-checksum-validated -- a
158
+ CREDENTIAL, not personal data, kept in this flag
159
+ rather than a new one to avoid flag proliferation
160
+ for the same "opt in, then checksum-validate"
161
+ shape). A different RISK CATEGORY, not a lower
162
+ confidence bar -- residoo is deliberately
163
+ credentials-only by default. Deliberately
164
+ excludes bare email/phone (too common in
165
+ ordinary text to meet this
160
166
  project's own high-confidence bar even opt-in).
161
167
 
162
168
  Watch:
@@ -227,22 +233,20 @@ Cred:
227
233
  tool, present only when RESIDOO_CRED_ALLOWED_COMMANDS is configured.
228
234
 
229
235
  Guard:
230
- residoo guard one binary, two Claude Code hooks, dispatched on
231
- the payload's own hook_event_name:
236
+ residoo guard one binary, three Claude Code hooks, dispatched
237
+ on the payload's own hook_event_name:
232
238
 
233
239
  PreToolUse blocks an obviously-sensitive file read
234
240
  (.env, id_rsa, .aws/credentials, and similar)
235
241
  before it can be written to the session
236
- transcript at all. Narrower than it sounds:
237
- Claude Code's hooks API can see a proposed Bash
238
- command or Read path before it runs, but never
239
- the command's OUTPUT, so this alone cannot catch
240
- a secret typed into a prompt or one arriving
241
- through an unrelated command's output.
242
-
243
- UserPromptSubmit closes exactly that gap: it
244
- checks the user's own typed prompt against
245
- residoo's 79 high-confidence rules (never
242
+ transcript at all. Narrower than it sounds on its
243
+ own: this only sees a proposed Bash command or
244
+ Read path before it runs, based on WHAT you're
245
+ about to touch, not what actually comes back.
246
+
247
+ UserPromptSubmit closes the "typed into a prompt"
248
+ gap: it checks the user's own typed prompt against
249
+ residoo's 84 high-confidence rules (never
246
250
  --verify, never --ocr -- this hook has no matcher
247
251
  and fires on every single prompt, so it must stay
248
252
  fast) and can block it before Claude processes it
@@ -258,15 +262,34 @@ Guard:
258
262
  consistent with this hook never blocking on
259
263
  anything it doesn't recognize.
260
264
 
265
+ PostToolUse closes the "arriving through a
266
+ command's output" gap for Bash specifically: it
267
+ scans the command's actual stdout/stderr against
268
+ the same 84 rules and, if one matches, replaces
269
+ the output with a redacted version before Claude
270
+ ever sees it (Claude Code's own updatedToolOutput
271
+ decision field). Bash only, disclosed rather than
272
+ silently partial: every other built-in tool's
273
+ exact output shape isn't documented precisely
274
+ enough to reconstruct a replacement safely, and a
275
+ wrong shape is silently ignored by Claude Code,
276
+ not reported as a failure. The command has
277
+ already run by this point -- this cannot undo a
278
+ file write or network call, only keep the raw
279
+ value out of the model's context.
280
+
261
281
  scan/watch/mcp remain the real safety net either
262
282
  way -- this is prevention on top of detection,
263
- not a replacement for it. Add both to
283
+ not a replacement for it. Add all three to
264
284
  .claude/settings.json:
265
285
  {"hooks":{
266
286
  "PreToolUse":[{"matcher":"Bash|Read",
267
287
  "hooks":[{"type":"command",
268
288
  "command":"residoo guard"}]}],
269
289
  "UserPromptSubmit":[{"hooks":[{"type":"command",
290
+ "command":"residoo guard"}]}],
291
+ "PostToolUse":[{"matcher":"Bash",
292
+ "hooks":[{"type":"command",
270
293
  "command":"residoo guard"}]}]}}
271
294
 
272
295
  Rotation:
package/src/guard.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * `residoo guard`: two Claude Code hooks in one binary, dispatched on the
4
+ * `residoo guard`: three Claude Code hooks in one binary, dispatched on the
5
5
  * payload's own `hook_event_name` field.
6
6
  *
7
7
  * PreToolUse blocks an obviously-sensitive file read before it happens,
@@ -11,14 +11,18 @@
11
11
  * from leaking": Claude Code's hooks API gives a PreToolUse hook the
12
12
  * PROPOSED tool input (a Bash command string, a Read file_path) before the
13
13
  * tool runs, and lets it deny the call outright -- but it never sees the
14
- * tool's OUTPUT, and by the time a PostToolUse hook fires, that output is
15
- * already committed to the transcript and can no longer be redacted. There
16
- * is no documented hook mechanism for "let the read happen, but strip the
17
- * secret out of what the model sees." So this can only block INPUT that
18
- * matches a known-sensitive file path pattern (.env, id_rsa, .aws/credentials,
19
- * and similar) -- it cannot catch a secret typed directly into a prompt, a
20
- * secret arriving in the output of an otherwise-unremarkable command
21
- * (curl, a build log), or any file path this pattern list does not name.
14
+ * tool's OUTPUT. So on its own, this can only block INPUT that matches a
15
+ * known-sensitive file path pattern (.env, id_rsa, .aws/credentials, and
16
+ * similar) -- it cannot catch a secret typed directly into a prompt, or one
17
+ * arriving in the output of an otherwise-unremarkable command (curl, `git
18
+ * log`, a build log), or any file path this pattern list does not name.
19
+ * UserPromptSubmit (below) closes the first gap; PostToolUse (below) closes
20
+ * the second, corrected from an earlier version of this comment that
21
+ * called it uncatchable -- Claude Code's own `updatedToolOutput` decision
22
+ * field (confirmed directly against code.claude.com/docs/en/hooks, not
23
+ * assumed) can in fact replace what the model sees, discovered during a
24
+ * later competitive-research pass after GitGuardian's ggshield was found to
25
+ * already scan tool output at this exact stage.
22
26
  *
23
27
  * UserPromptSubmit closes exactly the "typed directly into a prompt" gap
24
28
  * named above: Claude Code's own docs (code.claude.com/docs/en/hooks,
@@ -204,8 +208,86 @@ function evaluatePromptText(promptText) {
204
208
  }
205
209
 
206
210
  /**
207
- * Writes one structured audit line to stderr for a block decision --
208
- * CONTRIBUTING.md's own hard rule (rule 3) names `~/.residoo/rotations.json`
211
+ * Redacts every high-confidence match in `text`, tracking the first REAL
212
+ * (non-suppressed) hit for the audit trail/preview. Same suppression rule
213
+ * as evaluatePromptText (a documented vendor-example key or an obvious
214
+ * zero-entropy placeholder is never redacted) and the same rule set
215
+ * (PROMPT_GUARD_RULES) -- one bar for everything this module scans text
216
+ * content with, whether that content is a typed prompt or a command's
217
+ * output. `String.prototype.replace` with each rule's own global regex
218
+ * redacts every occurrence in one pass per rule; a value already inside a
219
+ * PREVIOUS rule's redaction (unlikely given how distinct these vendor
220
+ * prefixes are, but not impossible) is walked past unchanged rather than
221
+ * double-redacted, since only literal ORIGINAL text can still match a
222
+ * pattern from patterns.js.
223
+ */
224
+ function redactSecretsInText(text) {
225
+ if (typeof text !== "string" || !text) return { text: typeof text === "string" ? text : "", hit: null };
226
+ let result = text;
227
+ let hit = null;
228
+ for (const rule of PROMPT_GUARD_RULES) {
229
+ rule.re.lastIndex = 0;
230
+ result = result.replace(rule.re, (match) => {
231
+ if (VENDOR_EXAMPLE_VALUES.has(match) || zeroEntropyTail(match)) return match;
232
+ if (!hit) hit = { label: rule.label, preview: redact(match) };
233
+ return redact(match);
234
+ });
235
+ }
236
+ return { text: result, hit };
237
+ }
238
+
239
+ /**
240
+ * Pure decision function: given a PostToolUse hook payload's tool_name and
241
+ * tool_response, decide whether to redact. No I/O, fully unit-testable.
242
+ *
243
+ * Bash only, and stated why rather than left implicit: Claude Code's own
244
+ * docs (code.claude.com/docs/en/hooks, "PostToolUse decision control",
245
+ * fetched directly) state `updatedToolOutput` "must match the tool's
246
+ * output shape" and "a value that doesn't match the tool's output schema
247
+ * is ignored and the original output is used" -- a SILENT no-op, not an
248
+ * error residoo could detect and report. Bash's shape ({stdout, stderr,
249
+ * interrupted, isImage}) is the one built-in tool output shape those same
250
+ * docs actually publish; every other built-in tool's tool_response shape
251
+ * is not documented at this level of precision, so reconstructing one
252
+ * would risk exactly the silent-no-op failure this project's own
253
+ * discipline exists to avoid. A stated, narrow scope limit, not full
254
+ * "every tool's output" coverage -- and still a real, new defense layer:
255
+ * PreToolUse's matchSensitivePath blocks based on WHAT you're about to
256
+ * read (a path), this blocks based on WHAT ACTUALLY CAME BACK (content),
257
+ * catching a secret in `git log`, `env`, a build log, or any other Bash
258
+ * output PreToolUse's path-based check was never going to see coming.
259
+ *
260
+ * The tool has ALREADY run by the time this fires (Claude Code's own docs,
261
+ * same section) -- this cannot undo the read/write/network call the
262
+ * command made, and does not try to. It only keeps the raw value out of
263
+ * the model's context (and so out of the transcript this project's own
264
+ * `scan` exists to check), a narrower guarantee than PreToolUse's denial,
265
+ * disclosed as such rather than oversold.
266
+ */
267
+ function evaluatePostToolUse(toolName, toolResponse) {
268
+ if (toolName !== "Bash" || !toolResponse || typeof toolResponse !== "object") {
269
+ return { act: false, label: null, preview: null, updatedToolOutput: null };
270
+ }
271
+ const stdout = redactSecretsInText(toolResponse.stdout);
272
+ const stderr = redactSecretsInText(toolResponse.stderr);
273
+ const hit = stdout.hit || stderr.hit;
274
+ if (!hit) return { act: false, label: null, preview: null, updatedToolOutput: null };
275
+ return {
276
+ act: true,
277
+ label: hit.label,
278
+ preview: hit.preview,
279
+ updatedToolOutput: {
280
+ stdout: stdout.text,
281
+ stderr: stderr.text,
282
+ interrupted: !!toolResponse.interrupted,
283
+ isImage: !!toolResponse.isImage,
284
+ },
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Writes one structured audit line to stderr for a block OR redact decision
290
+ * -- CONTRIBUTING.md's own hard rule (rule 3) names `~/.residoo/rotations.json`
209
291
  * as "the only file residoo ever writes outside an explicit --seal...
210
292
  * nothing else may claim this carve-out," so this is NOT a new file, the
211
293
  * same choice `cred`'s own audit trail already made for the same reason
@@ -213,28 +295,45 @@ function evaluatePromptText(promptText) {
213
295
  * hook's own stderr at launch if you want it kept, same as `cred`.
214
296
  * Never the raw matched value -- `preview` is already redact()'d by the
215
297
  * caller (rule 4: no raw value in any log line, ever), and PreToolUse
216
- * decisions carry no value at all, only a path-pattern label.
298
+ * decisions carry no value at all, only a path-pattern label. `decision`
299
+ * defaults to "block" (every call site before PostToolUse existed) --
300
+ * PostToolUse passes "redact" since nothing is actually blocked there, the
301
+ * tool already ran; the log should say what really happened.
217
302
  */
218
- function logAuditLine(errOutput, { event, label, preview, sessionId, cwd }) {
303
+ function logAuditLine(errOutput, { event, label, preview, sessionId, cwd, decision = "block" }) {
219
304
  try {
220
305
  errOutput.write(JSON.stringify({
221
- ts: new Date().toISOString(), tool: "residoo guard", event, decision: "block",
306
+ ts: new Date().toISOString(), tool: "residoo guard", event, decision,
222
307
  label, ...(preview ? { preview } : {}), sessionId: sessionId || null, cwd: cwd || null,
223
308
  }) + "\n");
224
309
  } catch { /* stderr write failing is never a reason to fail the hook decision itself */ }
225
310
  }
226
311
 
227
312
  /**
228
- * Reads one PreToolUse OR UserPromptSubmit hook payload from `input`
229
- * (default stdin) -- distinguished by the payload's own `hook_event_name`
230
- * common field, a single binary handling both the way Claude Code's own
231
- * hook registration allows -- decides, and writes the hook's own JSON
232
- * response protocol to `output` (default stdout) -- exit code is the
233
- * caller's job (bin/residoo.js), this returns the intended process exit
234
- * code instead of calling process.exit itself, matching every other run*
235
- * function in cli.js. Every BLOCK decision also gets one structured line
236
- * on `errOutput` (default stderr) -- see logAuditLine's own docstring for
237
- * why stderr, never a file.
313
+ * Reads one PreToolUse, UserPromptSubmit, OR PostToolUse hook payload from
314
+ * `input` (default stdin) -- distinguished by the payload's own
315
+ * `hook_event_name` common field, a single binary handling all three the
316
+ * way Claude Code's own hook registration allows -- decides, and writes
317
+ * the hook's own JSON response protocol to `output` (default stdout) --
318
+ * exit code is the caller's job (bin/residoo.js), this returns the
319
+ * intended process exit code instead of calling process.exit itself,
320
+ * matching every other run* function in cli.js. Every BLOCK or REDACT
321
+ * decision also gets one structured line on `errOutput` (default stderr)
322
+ * -- see logAuditLine's own docstring for why stderr, never a file.
323
+ *
324
+ * The final `hook_event_name !== "PreToolUse"` guard fixes a real latent
325
+ * bug, not a hypothetical one: before PostToolUse existed here, anything
326
+ * OTHER than UserPromptSubmit fell through unconditionally to
327
+ * evaluateToolInput -- harmless while only PreToolUse and UserPromptSubmit
328
+ * were ever registered, but a PostToolUse payload also carries a
329
+ * `tool_input` field (Claude Code's own docs confirm both `tool_input` and
330
+ * `tool_response` are present), so without this guard a PostToolUse event
331
+ * would have been silently re-evaluated as a PreToolUse decision and could
332
+ * have emitted a `permissionDecision: "deny"` response for a tool call
333
+ * that had already finished executing -- a response Claude Code has no
334
+ * defined behavior for. Explicit is safer than "everything else falls
335
+ * through," the same fail-safe posture this whole module already commits
336
+ * to elsewhere.
238
337
  */
239
338
  async function runGuard({ input = process.stdin, output = process.stdout, errOutput = process.stderr } = {}) {
240
339
  const chunks = [];
@@ -266,6 +365,24 @@ async function runGuard({ input = process.stdin, output = process.stdout, errOut
266
365
  return 0;
267
366
  }
268
367
 
368
+ if (payload.hook_event_name === "PostToolUse") {
369
+ const decision = evaluatePostToolUse(payload.tool_name, payload.tool_response);
370
+ if (!decision.act) return 0;
371
+ logAuditLine(errOutput, {
372
+ event: "PostToolUse", label: decision.label, preview: decision.preview,
373
+ sessionId: payload.session_id, cwd: payload.cwd, decision: "redact",
374
+ });
375
+ output.write(JSON.stringify({
376
+ hookSpecificOutput: {
377
+ hookEventName: "PostToolUse",
378
+ updatedToolOutput: decision.updatedToolOutput,
379
+ },
380
+ }) + "\n");
381
+ return 0;
382
+ }
383
+
384
+ if (payload.hook_event_name !== "PreToolUse") return 0;
385
+
269
386
  const decision = evaluateToolInput(payload.tool_name, payload.tool_input);
270
387
  if (!decision.block) return 0;
271
388
 
@@ -283,4 +400,7 @@ async function runGuard({ input = process.stdin, output = process.stdout, errOut
283
400
  return 0;
284
401
  }
285
402
 
286
- module.exports = { evaluateToolInput, matchSensitivePath, evaluatePromptText, runGuard, SENSITIVE_PATH_PATTERNS };
403
+ module.exports = {
404
+ evaluateToolInput, matchSensitivePath, evaluatePromptText, evaluatePostToolUse,
405
+ runGuard, SENSITIVE_PATH_PATTERNS,
406
+ };
package/src/integrity.js CHANGED
@@ -677,6 +677,79 @@ function checkIntegrity({ home = os.homedir(), cwd = process.cwd(), projectMode
677
677
  }
678
678
  }
679
679
 
680
+ // ---- 5. credential-vault file permissions (HOME only, POSIX only) ------
681
+ // GitGuardian's "State of Secrets Sprawl 2026" (blog.gitguardian.com,
682
+ // published 2026-03-17) found 24,008 unique secrets in MCP-related config
683
+ // files on public GitHub, 8.8% of them still live -- these files
684
+ // routinely hold real, working credentials. This does not scan their
685
+ // CONTENT: the files below are the exact "credential VAULTS ... deliberately
686
+ // NOT read" set from agent-configs.js's own header comment, files whose
687
+ // whole documented job is holding the user's own keys/tokens -- reporting
688
+ // their content re-reports what the user put there on purpose. What's
689
+ // checked instead is narrower and complementary: whether the file's own OS
690
+ // permission bits leak that live credential to every other account on the
691
+ // machine. Anthropic's own docs (code.claude.com/docs/en/authentication,
692
+ // "Credential management", fetched 2026-09-04) state Claude Code writes
693
+ // .credentials.json with file mode 0600 on Linux and as the macOS
694
+ // Keychain-write-failure fallback, and that Windows inherits its user
695
+ // profile directory's own access controls. Kiro's own security guidance
696
+ // separately recommends "chmod 600" on its global mcp.json -- the
697
+ // vendor's own admission it holds secrets (already cited in
698
+ // agent-configs.js). Nothing here disputes that these tools do the right
699
+ // thing by default; the point is drift AFTER that -- a WSL DrvFs mount, a
700
+ // naive backup restore, or a shared-volume container mount can each
701
+ // silently widen a file's mode without the tool that wrote it ever
702
+ // knowing, the same way SSH itself checks id_rsa's permissions rather
703
+ // than trusting whoever created it. Windows is skipped entirely, not
704
+ // approximated: Node's fs.Stats.mode on Windows does not reflect NTFS
705
+ // ACLs, so a POSIX-style bit check there would be meaningless rather than
706
+ // merely imprecise. Project mode is skipped too -- none of these are ever
707
+ // project-scoped files by any vendor's own design, so there is no
708
+ // project-relative equivalent to check.
709
+ if (!projectMode && process.platform !== "win32") {
710
+ const geminiDir = process.env.GEMINI_CLI_HOME
711
+ ? path.join(process.env.GEMINI_CLI_HOME, ".gemini")
712
+ : path.join(home, ".gemini");
713
+ const credentialVaultFiles = [
714
+ {
715
+ file: path.join(process.env.CLAUDE_CONFIG_DIR || path.join(home, ".claude"), ".credentials.json"),
716
+ note: "Claude Code's OAuth login (code.claude.com/docs/en/authentication documents file mode 0600)",
717
+ },
718
+ {
719
+ file: path.join(process.env.CODEX_HOME || path.join(home, ".codex"), "auth.json"),
720
+ note: "Codex CLI's own auth store",
721
+ },
722
+ { file: path.join(geminiDir, "oauth_creds.json"), note: "Gemini CLI's own OAuth store" },
723
+ { file: path.join(geminiDir, ".env"), note: "Gemini CLI's own credential env file" },
724
+ {
725
+ file: path.join(home, ".kiro", "settings", "mcp.json"),
726
+ note: "Kiro's own security guidance recommends chmod 600 on this file",
727
+ },
728
+ ];
729
+ for (const { file, note } of credentialVaultFiles) {
730
+ const r = path.resolve(file);
731
+ if (seen.has(r)) continue;
732
+ seen.add(r);
733
+ let stat;
734
+ try { stat = fs.statSync(file); }
735
+ catch (err) {
736
+ mark(file, err && err.code === "ENOENT" ? "absent" : "unreadable");
737
+ if (!(err && err.code === "ENOENT")) {
738
+ add("warn", "unreadable-config", file, "credential file exists but its permissions could not be checked (stat failed): unverified, not clean");
739
+ }
740
+ continue;
741
+ }
742
+ mark(file, "checked");
743
+ const openBits = stat.mode & 0o077;
744
+ if (openBits !== 0) {
745
+ const octal = (stat.mode & 0o777).toString(8).padStart(3, "0");
746
+ const shown = display(file);
747
+ add("warn", "insecure-credential-permissions", file,
748
+ `${note}; current mode ${octal} grants group and/or other accounts on this machine access to a live credential. Fix: chmod 600 "${shown}"`);
749
+ }
750
+ }
751
+ }
752
+
680
753
  return {
681
754
  findings,
682
755
  filesChecked,
package/src/mcpTools.js CHANGED
@@ -365,7 +365,7 @@ function buildTools({ sources }) {
365
365
  projectDir: { type: "string", description: "Absolute path to a project/repo directory to scan instead of the machine-wide transcript stores (same as `residoo scan --project <dir>`). Omit for the default machine-wide scan." },
366
366
  includeNoisy: { type: "boolean", default: false, description: "Also run residoo's two low-confidence heuristic rules (generic password/secret assignments) -- catches more, false-positives more. Off by default." },
367
367
  includeSuppressed: { type: "boolean", default: false, description: "Include matches normally hidden because they look like vendor-documented example values or placeholder text. Off by default." },
368
- includePii: { type: "boolean", default: false, description: "Also scan for PII (US Social Security Numbers, Luhn-validated credit card numbers, checksum-validated IBANs) -- a different risk category from a credential, not a lower confidence bar. Off by default; residoo is deliberately credentials-only otherwise." },
368
+ includePii: { type: "boolean", default: false, description: "Also scan for PII and adjacent secrets (US Social Security Numbers, Luhn-validated credit card numbers, checksum-validated IBANs, BIP-39 checksum-validated crypto wallet seed phrases) -- a different risk category from a vendor credential, not a lower confidence bar. Off by default; residoo is deliberately credentials-only otherwise." },
369
369
  maxEntries: { type: "integer", minimum: 1, maximum: 200, default: 25, description: "Cap on distinct findings returned in full detail, pending-first. Counts in the response are always exact even when the entry list is truncated." },
370
370
  },
371
371
  required: [],
package/src/pii.js CHANGED
@@ -1,12 +1,16 @@
1
1
  "use strict";
2
2
 
3
+ const crypto = require("crypto");
4
+ const { BIP39_ENGLISH_WORDLIST } = require("./bip39wordlist");
5
+
3
6
  /**
4
- * Opt-in PII detection (--include-pii). Named directly by this session's
5
- * own competitive research into funded AI-DLP vendors (Strac, Cyberhaven,
6
- * Nightfall) as one of the few concrete, buildable things residoo could
7
- * adopt without becoming a hosted service -- and independently
8
- * corroborated by two direct competitors' own shipped detector lists
9
- * (DidILeak, Medusa), both of which cover PII alongside credentials.
7
+ * Opt-in PII-and-adjacent detection (--include-pii). Named directly by this
8
+ * session's own competitive research into funded AI-DLP vendors (Strac,
9
+ * Cyberhaven, Nightfall) as one of the few concrete, buildable things
10
+ * residoo could adopt without becoming a hosted service -- and
11
+ * independently corroborated by two direct competitors' own shipped
12
+ * detector lists (DidILeak, Medusa), both of which cover PII alongside
13
+ * credentials.
10
14
  *
11
15
  * Kept entirely separate from PATTERNS/NOISY_PATTERNS in patterns.js on
12
16
  * purpose: residoo's stated identity elsewhere in this project is
@@ -17,19 +21,34 @@
17
21
  * for a different reason (a different RISK CATEGORY, not a lower
18
22
  * confidence bar).
19
23
  *
20
- * Only three detectors, deliberately: DidILeak's own shipped list also
24
+ * "PII" is the name of the flag, not a perfectly accurate label for
25
+ * everything in it: a BIP-39 crypto wallet seed phrase (added after a
26
+ * competitive-feature-parity pass found AgentSweep ships seed-phrase
27
+ * detection and residoo didn't) is a CREDENTIAL, not personal data --
28
+ * arguably it belongs in the default, always-on set the way a vendor API
29
+ * key does. It lives here instead for the same architectural reason as
30
+ * the other three: no single-vendor prefix to anchor on (a seed phrase is
31
+ * 12-24 plain English words, structurally unlike every PATTERNS.js rule),
32
+ * and a real, own risk-category case for asking first, the same
33
+ * "different shape, not a lower bar" framing already applied to SSN/card/
34
+ * IBAN. Kept in the same flag rather than a new one to avoid flag
35
+ * proliferation for what is, mechanically, the same "opt in, then
36
+ * checksum-validate before ever reporting" pattern.
37
+ *
38
+ * Four detectors, deliberately not more: DidILeak's own shipped list also
21
39
  * includes bare email addresses and phone numbers, but both are far too
22
40
  * common in ordinary, non-sensitive text (a support email in a comment, a
23
41
  * phone number in an error message) to meet this project's own
24
42
  * "high-confidence only, a security tool that cries wolf gets
25
43
  * uninstalled" bar, opt-in or not -- DidILeak itself rates them "low"/
26
- * "info" severity for the same reason. The three included here all have a
44
+ * "info" severity for the same reason. Every detector here instead has a
27
45
  * REAL mathematical validator, not just a shape match, which is what
28
46
  * keeps false-positive risk low enough to ship even as an additive
29
- * category: Luhn for card numbers, ISO 7064 MOD 97-10 for IBAN, and the
30
- * Social Security Administration's own published invalid-range rules for
31
- * SSNs (no checksum exists for SSNs, hence "medium" confidence there, not
32
- * "high" -- disclosed, not smoothed over).
47
+ * category: Luhn for card numbers, ISO 7064 MOD 97-10 for IBAN, the BIP-39
48
+ * spec's own SHA-256 checksum for seed phrases, and the Social Security
49
+ * Administration's own published invalid-range rules for SSNs (no
50
+ * checksum exists for SSNs, hence "medium" confidence there, not "high"
51
+ * -- disclosed, not smoothed over).
33
52
  */
34
53
 
35
54
  /** Luhn checksum (ISO/IEC 7812-1): the standard validator for payment card numbers. `digits` must already be digits-only. */
@@ -72,6 +91,75 @@ function ibanValid(iban) {
72
91
  return mod === 1;
73
92
  }
74
93
 
94
+ const BIP39_INDEX = new Map(BIP39_ENGLISH_WORDLIST.map((w, i) => [w, i]));
95
+ const BIP39_VALID_LENGTHS = new Set([12, 15, 18, 21, 24]);
96
+
97
+ /**
98
+ * BIP-39 mnemonic checksum (github.com/bitcoin/bips/blob/master/bip-0039.mediawiki,
99
+ * fetched directly, cross-checked against the spec's own canonical all-zero
100
+ * test vector -- "abandon" x11 + "about" -- rather than trusted from memory):
101
+ * a mnemonic of MS words (12/15/18/21/24) encodes ENT bits of entropy plus a
102
+ * CS = ENT/32-bit checksum, each word an 11-bit index into the wordlist. The
103
+ * checksum is the first CS bits of SHA-256(entropy); this recomputes it and
104
+ * compares. English wordlist only -- BIP-39 also defines Japanese, Korean,
105
+ * Spanish, Chinese, French, Italian, Czech, and Portuguese wordlists this
106
+ * does not check, a disclosed scope limit rather than a silent one, the same
107
+ * shape as ibanValid's own per-country-length gap above.
108
+ *
109
+ * BigInt throughout: a 24-word phrase packs 264 bits, far past a safe JS
110
+ * integer, and a mis-sized intermediate here would silently corrupt every
111
+ * checksum it touches rather than throw.
112
+ */
113
+ function bip39ChecksumValid(words) {
114
+ if (!Array.isArray(words) || !BIP39_VALID_LENGTHS.has(words.length)) return false;
115
+ const indices = [];
116
+ for (const w of words) {
117
+ const idx = BIP39_INDEX.get(w);
118
+ if (idx === undefined) return false;
119
+ indices.push(idx);
120
+ }
121
+ const csBits = words.length / 3; // MS/3, derived from CS=ENT/32 and MS=(ENT+CS)/11
122
+ const entBits = words.length * 11 - csBits;
123
+
124
+ let combined = 0n;
125
+ for (const idx of indices) combined = (combined << 11n) | BigInt(idx);
126
+
127
+ const checksumMask = (1n << BigInt(csBits)) - 1n;
128
+ const checksumBits = combined & checksumMask;
129
+ const entropyBits = combined >> BigInt(csBits);
130
+
131
+ const entHex = entropyBits.toString(16).padStart(entBits / 8 * 2, "0");
132
+ const hash = crypto.createHash("sha256").update(Buffer.from(entHex, "hex")).digest();
133
+ const hashBits = BigInt("0x" + hash.toString("hex"));
134
+ const expectedChecksum = (hashBits >> BigInt(256 - csBits)) & checksumMask;
135
+
136
+ return checksumBits === expectedChecksum;
137
+ }
138
+
139
+ /**
140
+ * A real seed phrase is rarely the WHOLE candidate span: "here's my wallet
141
+ * seed: <12 words> keep it safe" is a plausible, ordinary way to paste one,
142
+ * and the candidate regex below (deliberately wide, to not miss a phrase
143
+ * that isn't sentence-initial) captures the surrounding words too. Slides
144
+ * every valid length (24 down to 12, longest first so a real 15+-word
145
+ * phrase is reported whole rather than as a coincidentally-checksum-valid
146
+ * 12-word prefix of it) across every starting offset in the captured run,
147
+ * returning the first exact substring whose checksum validates, or null.
148
+ * O(words x 5) checksum computations worst case -- cheap, since the regex
149
+ * itself already bounds "words" to at most 100.
150
+ */
151
+ function findBip39Phrase(candidateRun) {
152
+ const words = candidateRun.split(" ");
153
+ for (let start = 0; start < words.length; start++) {
154
+ for (const len of [24, 21, 18, 15, 12]) {
155
+ if (start + len > words.length) continue;
156
+ const slice = words.slice(start, start + len);
157
+ if (bip39ChecksumValid(slice)) return slice.join(" ");
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+
75
163
  const PII_PATTERNS = [
76
164
  // Dashed format only -- a bare 9-digit run is indistinguishable from
77
165
  // countless other numbers in a coding-agent transcript (ports, PIDs,
@@ -91,6 +179,21 @@ const PII_PATTERNS = [
91
179
  { id: "iban", label: "IBAN (checksum-validated)", confidence: "high",
92
180
  re: /\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b/g,
93
181
  validate: (m) => ibanValid(m) },
182
+ // Candidate: a run of 12-100 lowercase words, single-space-separated (how
183
+ // a seed phrase actually appears -- pasted as plain text, no punctuation
184
+ // breaking it up). Deliberately wider than the 12-24 a phrase itself can
185
+ // be: a real phrase is rarely the WHOLE sentence ("here's my seed: <12
186
+ // words> keep it safe"), so the candidate must be free to capture
187
+ // leading/trailing prose too -- findBip39Phrase (below) narrows it back
188
+ // down. validate() returns the narrowed substring, not a boolean: common
189
+ // English function words like "the"/"and"/"of"/"is" are NOT in the
190
+ // 2048-word list (confirmed by direct check against the fetched
191
+ // wordlist), so ordinary prose almost never survives even the membership
192
+ // test, let alone the checksum, but the narrowing still matters whenever
193
+ // it does.
194
+ { id: "crypto_seed_phrase", label: "Crypto wallet seed phrase (BIP-39, checksum-validated)", confidence: "high",
195
+ re: /\b[a-z]+(?: [a-z]+){11,99}\b/g,
196
+ validate: (m) => findBip39Phrase(m) },
94
197
  ];
95
198
 
96
- module.exports = { PII_PATTERNS, luhnValid, ibanValid };
199
+ module.exports = { PII_PATTERNS, luhnValid, ibanValid, bip39ChecksumValid, findBip39Phrase };
package/src/rotation.js CHANGED
@@ -1181,6 +1181,23 @@ const ROTATION_GUIDANCE = {
1181
1181
  ],
1182
1182
  revokeNote: "This passed the real ISO 7064 MOD 97-10 checksum every valid IBAN must satisfy, so it is very unlikely to be a random-looking placeholder.",
1183
1183
  },
1184
+ // Framed like every credential rule above, not like the two PII entries
1185
+ // just above it: unlike an SSN or an IBAN, a seed phrase is a bearer
1186
+ // credential with an actual "rotation" action available -- move the
1187
+ // funds to a new wallet -- so it belongs with "revoke and replace," not
1188
+ // "there is no console for this."
1189
+ crypto_seed_phrase: {
1190
+ label: "Crypto wallet seed phrase (BIP-39, checksum-validated)",
1191
+ consolePath: "No vendor console -- this is a self-custodied wallet's own master key, not an account credential a company can revoke for you.",
1192
+ steps: [
1193
+ "Treat the wallet as fully compromised the moment this leaves your control, even if you don't see funds move immediately -- anyone with the phrase can derive every key and address it controls",
1194
+ "Create a brand-new wallet from a freshly-generated seed phrase, on a device you trust, never by reusing or deriving from the exposed one",
1195
+ "Move all funds and any NFTs/tokens to the new wallet as soon as possible, paying attention to network fees so a move doesn't get front-run",
1196
+ "Update every dApp, exchange, or service that had the old wallet's address on file",
1197
+ "Remove the phrase from the transcript file if it doesn't need to be there -- residoo scan --seal quarantines the whole file without deleting anything, if you want a reversible first step",
1198
+ ],
1199
+ revokeNote: "This passed the real BIP-39 SHA-256 checksum every valid seed phrase must satisfy, so it is very unlikely to be a random-looking placeholder -- treat it as a real, live wallet key until shown otherwise.",
1200
+ },
1184
1201
 
1185
1202
  // ── NOISY_PATTERNS (only reachable via --include-noisy) ───────────────
1186
1203
  generic_password_assignment: {
package/src/scan.js CHANGED
@@ -609,21 +609,31 @@ async function scan({ sources, includeNoisy = false, includeSuppressed = false,
609
609
  // at all, so this mirrors decodeLine/ocrLine's simpler shape, not
610
610
  // matchLine's. `validate` (Luhn, IBAN's MOD 97-10) runs before a
611
611
  // candidate is even considered for suppression -- an invalid checksum
612
- // is not a "placeholder," it's simply not a match.
612
+ // is not a "placeholder," it's simply not a match. `validate` may return
613
+ // `true` (record the regex's own match verbatim -- Luhn/IBAN) or a
614
+ // narrower STRING (record that instead -- crypto_seed_phrase's
615
+ // findBip39Phrase, since the regex candidate deliberately over-captures
616
+ // surrounding prose and the checksum-valid phrase is usually a
617
+ // sub-string of it, not the whole match).
613
618
  const piiLine = (line, file, relFile, lineNo, mtimeMs) => {
614
619
  if (!includePii) return;
615
620
  for (const rule of PII_PATTERNS) {
616
621
  rule.re.lastIndex = 0;
617
622
  let m;
618
623
  while ((m = rule.re.exec(line)) !== null) {
619
- if (rule.validate && !rule.validate(m[0])) continue;
624
+ let value = m[0];
625
+ if (rule.validate) {
626
+ const v = rule.validate(m[0]);
627
+ if (!v) continue;
628
+ if (typeof v === "string") value = v;
629
+ }
620
630
  const before = line.slice(Math.max(0, m.index - CONTEXT_WINDOW), m.index);
621
- const suppressedReason = suppressionReason(m[0], before, rule.id);
631
+ const suppressedReason = suppressionReason(value, before, rule.id);
622
632
  if (suppressedReason && !includeSuppressed) {
623
633
  suppressedCount++;
624
634
  continue;
625
635
  }
626
- record(rule, m[0], relFile, file, lineNo, mtimeMs,
636
+ record(rule, value, relFile, file, lineNo, mtimeMs,
627
637
  suppressedReason ? "low" : rule.confidence, suppressedReason, { pii: true });
628
638
  }
629
639
  }
@@ -107,6 +107,18 @@ const { createInterface } = require("readline/promises");
107
107
  * (global config; their own security page recommends `chmod 600` on
108
108
  * it, a vendor admission it holds secrets) + named verbatim in JFrog's
109
109
  * Bitwarden-CLI target list.
110
+ * - `~/.mcp.json` (bare, directly under HOME, distinct from every
111
+ * subdirectory-nested path above) — Visual Studio (the full Windows
112
+ * IDE, not VS Code; VS Code's own MCP config is `.vscode/mcp.json`,
113
+ * already project-scoped below) documents this exact path as its
114
+ * global, all-solutions MCP config: Microsoft's own docs repo
115
+ * (github.com/MicrosoftDocs/visualstudio-docs, docs/ide/mcp-servers.md,
116
+ * fetched directly), "Serves as a global MCP server configuration for
117
+ * a specific user. Adding an MCP server here makes it load for all
118
+ * Visual Studio solutions." Visual Studio's remote-MCP OAuth tokens go
119
+ * through "the Visual Studio keychain" per the same doc -- an OS-backed
120
+ * store, not a plaintext file, so there is no separate credential
121
+ * vault to name here the way `.codex/auth.json` needed one.
110
122
  *
111
123
  * DELIBERATELY NOT READ, and why:
112
124
  * - `~/.claude/projects/**` AS SCAN CONTENT — claude-code.js's territory;
@@ -194,6 +206,7 @@ const CANDIDATES = [
194
206
  path.join(GEMINI_DIR, "settings.json"),
195
207
  path.join(CODEX_HOME, "config.toml"),
196
208
  path.join(KIRO_DIR, "settings", "mcp.json"),
209
+ path.join(HOME, ".mcp.json"),
197
210
  ];
198
211
 
199
212
  // Configs are KB-scale in every real observation this source's research
@@ -229,7 +242,12 @@ function available() {
229
242
  dirExists(CURSOR_DIR) ||
230
243
  dirExists(GEMINI_DIR) ||
231
244
  dirExists(CODEX_HOME) ||
232
- dirExists(KIRO_DIR)
245
+ dirExists(KIRO_DIR) ||
246
+ // Visual Studio (the full IDE) has no dedicated root directory the way
247
+ // every other tool above does -- its one machine-level artifact is this
248
+ // bare file, so it needs its own direct existence check rather than a
249
+ // dirExists() on some ~/.visualstudio root that doesn't exist.
250
+ fileExists(path.join(HOME, ".mcp.json"))
233
251
  );
234
252
  }
235
253
 
@@ -319,6 +337,13 @@ const PROJECT_CONFIG_RELPATHS = [
319
337
  ".mcp.json",
320
338
  path.join(".claude", "settings.json"),
321
339
  path.join(".claude", "settings.local.json"),
340
+ // Visual Studio's per-solution, VS-only MCP config: Microsoft's own docs
341
+ // (same source cited for ~/.mcp.json above) list this as a distinct
342
+ // location from <SOLUTIONDIR>/.mcp.json (source-controlled) --
343
+ // "Specific to Visual Studio and loads the specified MCP servers only
344
+ // for a specific user, for the specified solution," living inside the
345
+ // hidden, not-source-controlled .vs/ folder Visual Studio already owns.
346
+ path.join(".vs", "mcp.json"),
322
347
  ];
323
348
 
324
349
  // Probe bounds: transcript first-records are KB-scale; 256KB and 20 lines is