1. Executive Summary
GitMem is an MIT MCP server — around 59,000 lines of TypeScript —
that gives a coding agent "institutional memory": scars
(mistakes), wins, patterns and
anti-patterns, in Postgres with pgvector or a local
.gitmem/ directory.
Two mechanisms make it worth the report, and they are the same idea applied at both ends of the memory lifecycle.
At write time, a scar is refused unless it argues against itself.
function validateScar(params: CreateLearningParams): string[] {
const errors: string[] = [];
if (!params.severity) {
errors.push("Scars require severity (critical, high, medium, low)");
}
if (!params.counter_arguments || params.counter_arguments.length < 2) {
errors.push("Scars require at least 2 counter_arguments");
}
return errors;
}
You may not record a lesson learned without recording at least two
reasons someone might reasonably reject it. The counter-arguments are
stored in a TEXT[] column, folded into the embedding text,
and returned on every search — so the objection travels
with the claim to the point of use.
Nothing else in this atlas requires a memory to carry its own rebuttal.
At read time, every surfaced scar must be individually
answered. confirm_scars implements what the file
calls the refute-or-obey protocol:
"Each recalled scar must be addressed with:
APPLYING— Scar is relevant, past-tense evidence with artifact reference;N_A— Scar doesn't apply, scenario comparison required;REFUTED— Overriding scar, risk acknowledgment required."
And it is enforced, not requested.
hooks/scripts/recall-check.sh is a PreToolUse
hook on Bash with a "CONFIRMATION GATE (hard block,
consequential actions only)" that emits
{"decision": "block"} when recall-source scars are
unconfirmed. A sibling hook, credential-guard.sh,
hard-blocks any tool call that would expose credentials.
Daem0nMCP makes consultation a precondition. GitMem makes consultation per item: it is not enough to have called recall, you must say what you did about each thing recall told you, and overriding one costs you an explicit risk acknowledgment.
And the one clean bug is in the dismissal counter — section 9.
2. Mental Model
A scar is a structured artifact, not a sentence: severity,
problem_context, solution_approach,
applies_when, why_this_matters,
action_protocol, self_check_criteria, and the
counter-arguments. Recall surfaces it, the agent must answer it, and the
answer is recorded along with whether the resulting action
succeeded.
Diagram source
%% caption: a scar is refused without a severity and two counter-arguments, a PreToolUse hook blocks a consequential command while surfaced scars are unconfirmed, and a repeat mistake is recorded against the scar that failed to prevent it
flowchart TD
CL["create_learning(type='scar')"] --> V{"validateScar"}
V -->|"no severity, or fewer than 2 counter_arguments"| REJ["write refused, errors returned"]
V -->|ok| INS["gitmem_learnings row<br/>counter_arguments folded into the embedding text"]
Q["recall / search(project)"] --> S["surfaced scars, counter_arguments included"]
S --> CF["confirm_scars"]
CF --> A1["APPLYING — past-tense evidence + artifact reference"]
CF --> A2["N_A — scenario comparison required"]
CF --> A3["REFUTED — risk acknowledgment required"]
A1 --> ST["session state"]
A2 --> ST
A3 --> ST
BASH["agent attempts a consequential Bash call"] --> HK{"PreToolUse: recall-check.sh"}
HK -->|"recall-source scars unconfirmed"| BLK["{'decision':'block'}"]
HK -->|confirmed| GO["allowed"]
GO --> U["gitmem_scar_usage:<br/>reference_type explicit/implicit/acknowledged/refuted/none,<br/>surfaced_at, acknowledged_at, referenced, execution_successful"]
RM["the same mistake happens again"] --> RMF["repeat_mistake = true,<br/>related_scar_id, repeat_mistake_details"]
RMF --> AN["analytics: report the scars that failed to prevent recurrence"]3. Architecture
src/ splits into tools (the MCP surface),
schemas (Zod input schemas per tool),
services, hooks, commands,
diagnostics and constants. The storage layer
is Supabase/Postgres with pgvector on the paid path and a local
.gitmem/ directory on the free tier, and
search.ts implements both: a vector path
and a local keyword path that also scans decisions and merges them into
the same ranked list.
hooks/ ships a Claude Code plugin with
SessionStart, UserPromptSubmit,
PreToolUse and session-close hooks, plus templates for
Claude, Cursor, Copilot and Windsurf rule files.
Five separate vitest configs — unit, integration, e2e, perf and smoke — and 82 test files.
4. Essential Implementation Paths
Refuse a weak scar —
src/tools/create-learning.ts (validateScar
:39-51, buildEmbeddingText
:54-69, the early return on errors
:80-90).
Confirm a surfaced scar —
src/tools/confirm-scars.ts (the protocol docstring
:1-18), src/services/session-state.ts.
Block on unconfirmed —
hooks/scripts/recall-check.sh (the confirmation gate
:6-9), hooks/scripts/credential-guard.sh,
hooks/hooks/hooks.json (the PreToolUse matcher
on Bash).
Record what happened —
src/tools/record-scar-usage.ts,
record-scar-usage-batch.ts, schema/setup.sql
gitmem_scar_usage :132-147.
Suggest and dismiss —
src/services/thread-suggestions.ts
(SUGGESTION_MATCH_THRESHOLD :29, the match
loop :91-107, the new suggestion :120-127,
dismissSuggestionById :157-168,
getPendingSuggestions :174-180),
src/tools/dismiss-suggestion.ts.
5. Memory Data Model
gitmem_learnings is the table to read:
learning_type—CHECK IN ('scar', 'win', 'pattern', 'anti_pattern')severity—CHECK IN ('critical', 'high', 'medium', 'low')counter_arguments TEXT[]— the arguments against this memoryapplies_when TEXT[],problem_context,solution_approachwhy_this_matters,action_protocol,self_check_criteriais_active,decay_multiplierrepeat_mistake BOOLEAN,related_scar_id UUID,repeat_mistake_details JSONBproject,source_date,persona_name,embedding vector(1536)
repeat_mistake is the field to steal. A
scar exists to stop something happening again. This schema records the
case where it didn't — the flag, a link to the original scar,
and a JSONB detail with a reason — and
services/analytics.ts queries
repeat_mistake: "eq.true", joins
related_scar_id and repeat_mistake_details,
and reports them.
A memory system that measures which of its memories failed at their job is rare. The atlas has read many that measure retrieval and almost none that ask whether retrieval changed the outcome.
gitmem_scar_usage is the other half:
reference_type TEXT CHECK (reference_type IN
('explicit', 'implicit', 'acknowledged', 'refuted', 'none')),
surfaced_at TIMESTAMPTZ, acknowledged_at TIMESTAMPTZ,
referenced BOOLEAN, execution_successful BOOLEAN, variant_id UUID
'none' is the value most systems omit: a scar was shown
and the agent did nothing with it. surfaced_at and
acknowledged_at separate shown from addressed.
execution_successful closes the loop to the outcome, and
variant_id allows A/B variants of the same lesson.
6. Retrieval Mechanics
Vector search on the Supabase path, keyword on the free tier, with
severity and learning_type post-filters and a
fetchCount = matchCount * 3 over-fetch so the post-filter
has material to trim. Decisions are searched alongside learnings on the
local path and merged into one similarity-sorted list.
The project key is resolved, passed, and dropped.
Every caller computes it the same way —
params.project || getProject() || "default" — and hands it
to localScarSearch(query, fetchCount, project). That third
parameter is declared _project, and the function body is
two lines: fetch the singleton and call
instance.search(query, k). The key never reaches the
search. The file says so where the singleton is defined: "Unified
cache — all scars loaded into single instance regardless of project. At
~400 scars, semantic similarity handles relevance better than project
partitioning. Project params kept in signatures for backward compat but
ignored for cache lookup." getLocalVectorSearch,
initializeLocalSearch,
reinitializeLocalSearch, isLocalSearchReady
and getCacheMetadata all take the same underscore-prefixed,
ignored argument, and both call sites in startup.ts pass
undefined anyway, over a load the comment labels "Load
ALL scars from Supabase (cross-project unified cache)".
The remote fallback is explicit about it rather than accidental.
project_filter is deliberately not sent, and the comment
gives the reasoning: the primary path this stands in for is the unified
cross-project cache, so filtering here "would make the fallback
return a different, narrower result set than the path it stands in for —
a silent behaviour change on exactly the cold-start calls that are
hardest to notice." That is a defensible choice given a
cross-project primary, and it is the second place the key is dropped on
purpose.
So retrieval is cross-project by design, at about four hundred scars,
on the argument that semantic similarity beats partitioning at that
size. It is a real position and it is not scope enforcement: no read
path applies a stored project key, and scope_enforced is
not earned.
counter_arguments come back on every result shape in
search.ts. That is the detail that makes the write-time
rule worth having: the objection is not filed away, it is injected next
to the claim.
7. Write Mechanics
create_learning validates, embeds and inserts. A scar
failing validation returns success: false with the errors
and no row — a refusal, not a warning.
archive_learning sets is_active = false,
and log.ts filters is_active: "eq.true", so
archiving removes a learning from retrieval without deleting it. Nothing
is keyed on a rejected value: an archived scar's content,
re-submitted, is a new row.
8. Agent Integration
An MCP server on npm (gitmem-mcp) with a one-command
init wizard that auto-detects the IDE, a Claude Code plugin
with four lifecycle hook points, rule templates for four editors, a
PRIVACY.md and a SECURITY.md, and a starter
scar set in schema/starter-scars.json.
The hook set is the integration that matters:
SessionStart initialises, UserPromptSubmit
auto-retrieves, PreToolUse guards credentials and enforces
confirmation, and a session-close check runs at the end.
9. Reliability, Safety, and Trust
Scope enforced — withheld, per section 6. The key is
stored on the row and resolved on every call, and the search functions
take it as _project and ignore it in favour of one
cross-project cache. A parameter that reaches a signature and stops
there is the read-path form of a declared-and-unwired mechanism, and the
project states the design in the source rather than leaving it to be
inferred.
Human review — awarded on the suggestion pipeline:
implicit thread detection proposes a thread from session-embedding
similarity, the suggestion is status: "pending", and it
becomes an open thread only when a person promotes it or disappears when
a person dismisses it. A proposal that does not take effect until
someone acts on it is the mark's substance.
Audit log — withheld, deliberately.
gitmem_scar_usage is the richest retrieval-feedback table
in this corpus, and retrieval feedback is explicitly the half the mark
does not cover: it records what was surfaced and used, not an
append-only record of memory mutations. Nothing found logs
creates, archives and edits as events.
Trust state — withheld. is_active is a
lifecycle flag and severity and
decay_multiplier are grades, not epistemic states. The
nearest thing to an epistemic state is repeat_mistake,
which describes the world rather than the memory's standing.
Tombstone, bitemporal, negative eval — no.
source_date is stored and no query treats it as a validity
bound.
A retrieval fallback answered PGRST202 on every call for as
long as it existed, and the repository is the source for that.
The Supabase scar-search fallback built its RPC name from the table
prefix with a verb appended — gitmem_scar_search by
default, orchestra_scar_search under a prefix — and
PostgREST exposes neither; the deployed functions are
match_<table> and
match_<table>_weighted. The comment left behind
states the consequence and why it went unnoticed: the call "returned
PGRST202 on every call, on every deployment, since it was written —
invisible because it is only reached while the local vector index is
cold." A path that runs only during warm-up, and returns an error
the caller treats as no results, is indistinguishable from a
genuine miss; the pair of names differ in more than spelling, since the
weighted variant is the one returning the decay_multiplier
recall consumes and it takes match_threshold where its
sibling takes similarity_threshold. A committed test pins
the names.
The confirmation gate could be satisfied by a retrieval that
failed. confirm_scars is the enforcement point for
the refute-or-obey protocol, and a fix titled "confirm_scars must
not green-light a failed retrieval" separates the two cases with a
test beside it. A review gate whose precondition is a successful recall
has to be able to tell a failed recall from an empty one, and that
distinction is the same one the RPC bug made invisible.
The dismissal counter cannot reach its own threshold.
dismiss_suggestion's header says: "Suggestions
dismissed 3+ times are permanently suppressed."
getPendingSuggestions implements it:
s.status === "pending" && s.dismissed_count < 3.
But dismissSuggestionById sets
suggestion.status = "dismissed" and increments the count,
and the routine that decides whether an incoming topic matches an
existing suggestion begins:
for (const suggestion of updated) {
if (suggestion.status !== "pending") continue;
A dismissed suggestion is skipped by the matcher. So the next time
the same topic recurs, no existing suggestion matches, and a fresh one
is created with generateSuggestionId() and
dismissed_count: 0. In the paths read, nothing returns a
dismissed suggestion to pending.
Therefore dismissed_count can never exceed 1,
the < 3 guard is unreachable, and dismissal suppresses a
record rather than a topic — the user who dismisses a
suggestion will be offered the same topic again under a new id. The unit
test asserts exactly the reachable behaviour
(expect(result! .dismissed_count).toBe(1)), which is why
the gap is invisible to the suite.
The fix is small: match against dismissed suggestions too (they carry the embedding), and increment the existing record instead of creating a new one. That would also turn the feature into something this atlas has been looking for — a rejected item keyed on its content rather than its id, which is the property that separates a real suppression from a bypassable one.
10. Tests, Evals, and Benchmarks
No paper, no retrieval benchmark. 92 test files
across five vitest configs — unit, integration, e2e, perf and smoke —
plus CI, a CHANGELOG.md, a SECURITY.md, a
PRIVACY.md, a CODE_OF_CONDUCT.md and a
DIRECTORY-SUBMISSIONS.md.
Notable test names: no-console-log.test.ts (a lint
invariant asserted as a test), provenance-citation.test.ts,
confirm-scars.test.ts,
archive-learning.test.ts,
thread-suggestions.test.ts.
The measurement that would matter here is the one the schema is built
for and no committed analysis performs:
gitmem_scar_usage.execution_successful joined against
reference_type, answering whether confirming a scar changed
the outcome. The repeat_mistake analytics report is the
closest, and it counts recurrences rather than comparing them against a
baseline.
I ran nothing.
11. For Your Own Build
Steal
- Require counter-arguments before you accept a lesson. Two, minimum, enforced at write time with the write refused otherwise. It forces the author to have thought about when the lesson does not apply, and it is the cheapest guard against a memory system that accumulates confident overgeneralisations.
- Return the counter-arguments with the memory. Storing the objection and hiding it at retrieval would be pointless; here it rides along in every result shape and in the embedding text.
- Make consultation per item, not per session. Every
surfaced scar answered with
APPLYING,N_AorREFUTED— and make each answer cost something: past-tense evidence with an artifact reference, a scenario comparison, or an explicit risk acknowledgment. - Enforce it with a hard block on consequential actions
only.
{"decision": "block"}from aPreToolUsehook matched toBash, with read-only tools unaffected, is the right blast radius. - Record
'none'as a usage outcome. A scar that was surfaced and ignored is data; a table that only records uses cannot tell you which memories are dead weight. - Separate
surfaced_atfromacknowledged_at. Two timestamps make "shown" and "addressed" distinguishable, which is what makes the ignore rate measurable. - Track whether the scar failed.
repeat_mistake,related_scar_idand a reason, reported in analytics. A memory system should be able to name its own memories that did not work. - Give a learning an
action_protocolandself_check_criteria. A lesson an agent can act on beats a lesson it can only read. - Over-fetch before post-filtering.
matchCount * 3when a severity or type filter is set.
Avoid
- Do not skip non-pending records in a dedup matcher when the non-pending state is what you are trying to enforce. Here it makes a documented suppression rule unreachable and turns "dismissed permanently" into "dismissed once, then offered again".
- Do not key a suppression on a generated id. Key it on the content or its embedding, or a re-derivation walks straight past it.
- Do not let a test assert only the reachable branch.
The suite checks
dismissed_countbecomes 1 and never asks how it would become 3.
Fit
The best fit in this batch for a team that wants an agent to be accountable to its memory rather than merely informed by it — the refute-or-obey gate plus the counter-argument requirement is a coherent discipline, and the hooks make it real rather than advisory.
The Supabase dependency is the main adoption question; the free tier's local keyword path exists but is a different retrieval quality.
12. Open Questions
- Who sets
repeat_mistake? The analytics read it and the write path was not located; whether it is agent-reported or detected was not established. - What is
variant_idfor?gitmem_scar_usagesupports A/B variants of a scar and no variant assignment logic was found. - Is
execution_successfulever analysed? It is the column that would answer whether confirmation changes outcomes. - Does
decay_multiplierreach ranking? It is on the row; its consumer was not traced.
Appendix: File Index
Write-time validation —
src/tools/create-learning.ts (validateScar
:39-51, embedding text including counter-arguments
:54-69, the refusal path :80-90)
Confirmation protocol —
src/tools/confirm-scars.ts (the refute-or-obey docstring
:1-18), src/schemas/confirm-scars via
src/schemas/index.ts,
src/services/session-state.ts
Hooks — hooks/hooks/hooks.json
(SessionStart, UserPromptSubmit,
PreToolUse on Bash),
hooks/scripts/recall-check.sh (the confirmation gate
:6-9, the output contract :23),
hooks/scripts/credential-guard.sh (the
"decision": "block" emissions),
hooks/scripts/session-close-check.sh
Schema — schema/setup.sql
(gitmem_learnings :11-39,
gitmem_sessions :68-85,
gitmem_decisions :104-118,
gitmem_scar_usage :132-150),
schema/starter-scars.json
Retrieval — src/tools/search.ts
(project resolution :131, over-fetch :136, the
local path :140-200, counter-arguments in every result
shape :151, :269, :301)
Suggestions —
src/services/thread-suggestions.ts
(SUGGESTION_MATCH_THRESHOLD :29, the thread
cover check :80-88, the pending-only match loop
:91-107, the new suggestion :120-127,
dismissSuggestionById :157-168,
getPendingSuggestions :174-180),
src/tools/dismiss-suggestion.ts (the suppression claim
:4-5),
tests/unit/services/thread-suggestions.test.ts
(:271-278)
Analytics — src/services/analytics.ts
(repeat_mistake: "eq.true" :229, the select
:236, the report :357-361,
:808)
History
2026-09-10 — d47a625f…
— read again, 19 commits past the previous pin, and
scope_enforced is withdrawn as a first-reading
error. The mark was awarded on project being
resolved per call and passed into
localScarSearch(query, fetchCount, project); that third
parameter is _project and the body calls
instance.search(query, k), so the key stops at the
signature. The singleton it fetches is one cross-project cache —
"all scars loaded into single instance regardless of project…
Project params kept in signatures for backward compat but ignored for
cache lookup" — filled by two startup.ts calls that
pass undefined over a load labelled "Load ALL scars
from Supabase (cross-project unified cache)", and the remote
fallback omits project_filter deliberately so it matches
the path it stands in for. None of this moved between the pins:
src/services/local-vector-search.ts is unchanged since the
previous reading and its last commits date to February 2026, one of them
titled "fix: session refresh project context, cross-project recall,
and thread cascade". The claim was wrong when written, in the
direction of crediting a mechanism the code documents itself as not
having. human_review holds and carries an evidence record.
Two defects in the mechanism paths were fixed upstream and are recorded
here as the project's own history: the scar-search fallback had built
its RPC name from the table prefix with a verb appended and
"returned PGRST202 on every call, on every deployment, since it was
written", and confirm_scars could green-light a failed
retrieval. The published dismissed_count defect was re-run
and stands — dismissal sets the status as well as the counter, so the
< 3 guard is still unreachable. Test files went from 82
to 92. Screened before reading: two auto-run surfaces, no manifest
inside the seven-day cooldown, one build-time execution path and two
unpinned dependency surfaces; nothing was installed, built or run.
2026-08-09 — c091a758…
— first reading. Screened before reading; the tree was read, never
installed, and no test was run.