DEV Community

473185670
473185670

Posted on

How I Built an Imposter Thought Detector in 60 Lines of Vanilla JavaScript — No AI, No NLP, No Sentiment Analysis

When you build anything that reads someone's thoughts and says "that's an imposter thought," the assumption is immediate: you need sentiment analysis. An LLM. A fine-tuned classifier. Something that "understands" the nuance of self-doubt.

You don't.

I built a tool that takes a thought — "They're going to find out I don't actually know what I'm doing" — and classifies it into the specific cognitive distortion driving it — Fortune-Telling (predicting a future exposure catastrophe) — then generates the targeted CBT reframe, using a domain-adapted keyword mapper. Zero dependencies, zero API calls, zero ML. It runs in the browser, on private thoughts, with no data leaving the device.

This is how imposter syndrome detection becomes 60 lines of JavaScript — and why a domain-adapted rule-based classifier beats both a generic distortion detector and a language model for a specific, well-studied psychological pattern.

The problem

Imposter syndrome isn't just "self-doubt." It's a cluster of specific, recognizable cognitive distortions that fire in predictable ways:

  • "I'm a fraud" → that's Labeling (a global identity label, not a fact)
  • "I just got lucky" → that's Discounting Positives / External Attribution (erasing your own skill)
  • "One day they'll find out" → that's Fortune-Telling (predicting a catastrophe without evidence)
  • "They think I'm smart but if they knew..." → that's Mind-Reading (assuming others' hidden evaluation)
  • "I'm not as smart as everyone else here" → that's All-or-Nothing / Social Comparison

A generic cognitive distortion detector — the kind that classifies any thought into the standard 11 distortions — can catch some of these. But it misses the pattern. "I just got lucky" gets classified as "Disqualifying the Positive" — technically correct, but uselessly generic. The imposter-specific reframe is different: it's not "consider the positive evidence" (the generic advice), it's "skill is the common factor across your achievements; luck is occasional — attribute success to its real causes." The taxonomy and the intervention are both domain-specific.

Try it live: CBT for Imposter Syndrome — Impostor Thought Tracker

The technique: domain-adapted classification

This is the key idea, and it's borrowed from ML without using ML.

In machine learning, transfer learning / domain adaptation means: take a general model trained on broad data, then adapt it to a specific domain by swapping in domain-specific knowledge — without changing the architecture. The structure stays; the weights change.

The rule-based equivalent is simpler and more transparent: keep the keyword-matching architecture, swap the knowledge base. A generic distortion detector has 11 keyword sets mapping to 11 distortions. An imposter detector has 7 imposter-specific keyword sets mapping to 7 imposter-adapted distortion variants, each with a reframe written for the imposter context.

Same algorithm. Different knowledge base. That's domain adaptation without a single gradient step.

This is a fundamentally different design from the generic detector I built before. The generic one asks "which of the 11 standard distortions is this?" The imposter one asks "which of the 7 ways does imposter syndrome specifically distort thinking — and what is the imposter-specific reframe?" The architecture is identical (keyword → category + reframe). The taxonomy is the product.

Why not an LLM?

I considered it. Here's why I didn't:

  1. The output space is small and domain-known. Decades of CBT research have mapped how imposter syndrome distorts thinking. There are ~7 recurring patterns. An LLM generating one of 7 labels from a 175-billion-parameter model is a sledgehammer on a thumbtack.
  2. The classification must be deterministic. Run the detector on "I just got lucky" twice and you must get "Discounting Positives / External Attribution" both times — that's what makes it a tool someone can trust and learn from. An LLM's output is stochastic. For a technique meant to teach someone to recognize their own patterns, nondeterminism is a bug.
  3. Privacy is the constraint. People type "I'm a fraud and everyone is about to find out" into this. That sentence sent to an API is a data breach dressed as a feature. Client-side keyword matching means the thought never leaves the browser.
  4. The reframe is the intervention — and it must be clinically precise. An LLM-generated reframe might be plausible but subtly wrong (e.g., "just be confident!" — that's not CBT, that's toxic positivity). The rule-based reframes are written by hand, grounded in the actual CBT technique, and medically honest. For a mental health tool, "sounds right" isn't good enough.
  5. Domain adaptation is a knowledge-base swap, not an architecture change. Adapting an LLM to imposter syndrome means fine-tuning, eval sets, drift monitoring, cost. Adapting a rule-based classifier means editing 7 keyword arrays. The maintenance cost difference is 10,000x.

For a known output space, a deterministic classification, a privacy-first context, and a domain that's already been mapped by clinical research, a domain-adapted keyword mapper isn't a downgrade from an LLM — it's the correct architecture.

The algorithm: 7 imposter-specific branches

The core is a single function. Seven keyword sets, seven imposter-adapted distortion variants, seven hand-written reframes:

function dwIdentifyDistortion(thought) {
  var t = (thought || '').toLowerCase();
  if (!t) return { name: 'unidentified', reframe: 'Notice the thought, then identify the distortion.' };

  // 1. Global identity label
  if (t.indexOf('fraud') > -1 || t.indexOf('fake') > -1 || t.indexOf('phony') > -1 || t.indexOf('pretend') > -1) {
    return { name: 'Labeling',
      reframe: '"Fraud" is a global label, not a fact. You are a person with specific competencies ' +
               'and specific gaps — like everyone. Replace it with: "I am competent in [X], still ' +
               'developing in [Y], and that is normal." The evidence log is the antidote to the label.' };
  }

  // 2. Erasing your own skill
  if (t.indexOf('lucky') > -1 || t.indexOf('fluke') > -1 || t.indexOf('right place') > -1 || t.indexOf('anyone could') > -1) {
    return { name: 'Discounting Positives / External Attribution',
      reframe: 'Attributing success to luck is discounting positive evidence. If it were truly only ' +
               'luck, you would not succeed consistently while others don\'t. Skill is the common ' +
               'factor; luck is occasional. Ask: what did I specifically do that contributed?' };
  }

  // 3. Predicting future exposure
  if (t.indexOf('found out') > -1 || t.indexOf('exposed') > -1 || t.indexOf('one day') > -1 || t.indexOf('eventually') > -1) {
    return { name: 'Fortune-Telling',
      reframe: '"One day I\'ll be found out" is fortune-telling — predicting a future catastrophe ' +
               'without evidence. Found out based on what? You produce real work that meets real ' +
               'standards. The exposure you fear has not happened because there is nothing to expose.' };
  }

  // 4. Assuming others' hidden evaluation
  if (t.indexOf('they think') > -1 || t.indexOf('if they knew') > -1 || t.indexOf('everyone thinks') > -1) {
    return { name: 'Mind-Reading',
      reframe: 'Assuming you know what others think is mind-reading. Their evaluation is based on ' +
               'your observable output. If competent people assess your work as competent, trust ' +
               'the external evidence over your internal feeling. Or better: ask them.' };
  }

  // 5. Comparing your insides to their outsides
  if (t.indexOf('not smart') > -1 || t.indexOf('don\'t belong') > -1 || t.indexOf('everyone else') > -1 || t.indexOf('not as') > -1) {
    return { name: 'All-or-Nothing / Social Comparison',
      reframe: 'You compare your anxious internal process to their polished external result. ' +
               'Competence is not a binary; it is a profile with strengths and gaps. You belong ' +
               'because you were selected — someone with judgment chose you over alternatives.' };
  }

  // 6. Questioning deservingness
  if (t.indexOf('deserve') > -1 || t.indexOf('earned') > -1 || t.indexOf('belong') > -1) {
    return { name: 'Personalization / Self-Blame',
      reframe: 'Questioning whether you "deserve" your position sets an impossible standard. You ' +
               'were selected through a process that evaluated you against criteria. Your feeling ' +
               'of not deserving does not override their evidence-based decision.' };
  }

  // 7. Arbitrary competence standards
  if (t.indexOf('should') > -1 || t.indexOf('supposed to') > -1 || t.indexOf('ought') > -1) {
    return { name: 'Should Statements',
      reframe: '"I should know this already" is a should statement — an arbitrary standard that ' +
               'generates fraud feelings. Competence develops through practice, not on a schedule. ' +
               'Replace "I should be X" with "I am at Y, developing toward X — the normal trajectory."' };
  }

  // Fallback: the thought discounts competence but doesn't match a specific pattern
  return { name: 'Discounting Positives (general)',
    reframe: 'This thought discounts your demonstrated competence. Read your evidence log: the ' +
             'fraud thought cannot survive contact with a written, dated, verifiable record.' };
}
Enter fullscreen mode Exit fullscreen mode

Seven branches. Each one is a recognition pattern (the keywords imposter thoughts actually use) mapped to a name (the distortion) and a reframe (the imposter-specific intervention). The fallback isn't "I don't know" — it's the most common imposter pattern (general competence discounting) with the universal antidote (the evidence log).

Notice what's NOT here: no confidence score, no probability, no "match strength." A keyword either fires or it doesn't. This is deliberate. In a clinical technique, the output is "this is Fortune-Telling" — not "73% Fortune-Telling, 12% Mind-Reading." False precision is dishonest. The honest output is the category the thought matches, the name of the distortion, and the reframe. That's it.

The behavioral insight layer

Detecting the distortion is half the tool. The other half is what you do about it — and imposter syndrome has characteristic behavioral responses that are just as classifiable as the thoughts:

// Self-deprecation / deflection rate
// (dismissed praise + attributed to luck + sought reassurance)
var deflectCount = dwEntries.filter(function(e) {
  var b = (e.behavior || '').toLowerCase();
  return b.indexOf('self-deprec') > -1 || b.indexOf('luck') > -1 || b.indexOf('reassur') > -1;
}).length;
var deflectRate = Math.round(deflectCount / total * 100);

// Avoidance rate (avoided + procrastinated)
var avoidCount = dwEntries.filter(function(e) {
  var b = (e.behavior || '').toLowerCase();
  return b.indexOf('avoid') > -1 || b.indexOf('procrast') > -1;
}).length;
var avoidRate = Math.round(avoidCount / total * 100);

// Overwork / compensation rate
var overworkCount = dwEntries.filter(function(e) {
  return (e.behavior || '').toLowerCase().indexOf('overwork') > -1;
}).length;
var overworkRate = Math.round(overworkCount / total * 100);

// "Owned it" rate (accepted praise + self-compassion)
var ownedCount = dwEntries.filter(function(e) {
  var b = (e.behavior || '').toLowerCase();
  return b.indexOf('accepted praise') > -1 || b.indexOf('self-compassion') > -1;
}).length;
var ownedRate = Math.round(ownedCount / total * 100);
Enter fullscreen mode Exit fullscreen mode

Four behavioral rates, each a keyword filter over the logged actions. Together they paint the behavioral profile: are you deflecting, avoiding, overworking, or — the adaptive response — owning it? The ownedRate is the recovery metric. When it rises and deflectRate falls, the evidence log is working.

And a belief trend, computed the same way as in the other trackers — split-half comparison across chronologically-sorted entries:

var chrono = dwEntries.slice().sort(function(a, b) {
  return new Date(a.when) - new Date(b.when);
});
if (chrono.length >= 4) {
  var half = Math.floor(chrono.length / 2);
  var firstAvg  = chrono.slice(0, half).reduce(function(s, e) { return s + e.belief; }, 0) / half;
  var secondAvg = chrono.slice(half).reduce(function(s, e) { return s + e.belief; }, 0) / (chrono.length - half);
  if (secondAvg < firstAvg - 5) trendStr = 'decreasing (the evidence is working — keep going!)';
  else if (secondAvg > firstAvg + 5) trendStr = 'increasing (new/challenging situation? that\'s normal)';
}
Enter fullscreen mode Exit fullscreen mode

Split-half trend detection: compare the average belief intensity in the first half of entries to the second half. If the second half is ≥5 points lower, belief is decreasing (the intervention is working). Needs ≥4 entries to be meaningful — honest about when it doesn't know.

The results

The detector handles real imposter thoughts:

Input:  "I just got lucky they hired me. Anyone could do this job."
Match:  "lucky" + "anyone could"  →  Discounting Positives / External Attribution
Reframe: "If it were truly only luck, you would not succeed consistently
         while others don't. Skill is the common factor. Ask: what did I
         specifically do that contributed?"

Input:  "One day they'll find out I don't really know React."
Match:  "find out" + "one day"  →  Fortune-Telling
Reframe: "Found out based on what? You produce real work that meets real
         standards. The exposure you fear has not happened because there
         is nothing to expose."

Input:  "If they knew how long that bug took me, they'd think I'm slow."
Match:  "if they knew" + "they'd think"  →  Mind-Reading
Reframe: "Their evaluation is based on your observable output. If competent
         people assess your work as competent, trust the evidence. Or ask them."
Enter fullscreen mode Exit fullscreen mode

And the behavioral profile across a month of entries:

30 entries logged.
  Deflection rate:  67%  (dismissed praise, attributed to luck, sought reassurance)
  Avoidance rate:   20%  (avoided, procrastinated)
  Overwork rate:    10%  (overworked to compensate)
  Owned-it rate:     3%  (accepted praise, self-compassion)
  Belief trend:     stable
→ Profile: classic imposter loop — deflect the wins, avoid the risks, overwork
  the gaps, never own the credit. The 3% owned-it rate is the lever.
Enter fullscreen mode Exit fullscreen mode

Total code: ~60 lines (classifier + behavioral rates + trend). Total dependencies: 0. Total API calls: 0. Total thoughts sent to a server: 0.

Takeaways for builders

  1. Domain adaptation is a knowledge-base swap, not an architecture change. The imposter detector uses the exact same keyword-matching architecture as the generic distortion detector. The only difference is the 7 keyword arrays and 7 reframes. When someone says "we need to fine-tune a model for our domain," ask first: is our domain already mapped by existing research? If yes, a rule-based knowledge-base swap is 10,000x cheaper and more transparent.
  2. The generic classifier is the wrong abstraction for a known domain. A generic 11-distortion detector classifies "I just got lucky" as "Disqualifying the Positive." Technically correct, uselessly generic. The imposter-specific variant classifies it as "Discounting Positives / External Attribution" and gives the imposter-specific reframe ("skill is the common factor"). The taxonomy IS the product.
  3. Honest fallbacks beat confabulation. The default branch returns "Discounting Positives (general)" with the evidence-log antidote — not "unidentified" and not a hallucinated category. A tool that always has an answer builds dependence; a tool that names the most likely pattern and admits it's general builds skill.
  4. Behavioral rates are a second classifier over the same data. The deflection/avoidance/overwork/owning rates are just keyword filters over the behavior field — a second classification pass on a different dimension of the same entries. Two classifiers, one data structure, zero extra storage. The ownedRate is the single most important number in the tool: it's the recovery metric.

If you want to see the full code or use the tools:

If you prefer a structured Notion template for tracking your thoughts (the "paper notebook" version of these tools), I made one: CBT Thought Record for Notion ($7).


What's a domain where you're about to fine-tune an LLM — but the patterns are already mapped by existing research, and a 60-line knowledge-base swap would do?

Top comments (0)