Skip to content

Releases: SantyagoSeaman/sidegraph

v0.14.0

Choose a tag to compare

@SantyagoSeaman SantyagoSeaman released this 16 Oct 23:52

Sidegraph 0.14.0 preserves anchors and domains across Graphify 0.9.90, fixes store
setup and policy selection, and adds OpenCode V2 plus experimental Muse packaging.

Before updating an existing store, follow the migration guide:
update every Sidegraph client, back up the store, snapshot domains, then upgrade Graphify.
The RC tag stays immutable; RC installations keep their explicit RC source until you
return to the stable installation/profile. See the installation and update commands.

Claude Code and Codex pass the full native gate on Windows x64 and macOS ARM with
Python 3.13/3.14, including Windows PowerShell 5.1 without PowerShell 7. Muse remains
experimental MCP/shared skills; OpenCode V2 supplies MCP/shared skills. Their existing
macOS and owner field receipts do not establish Windows lifecycle parity.

[0.14.0] — 2026-10-16

Graphify 0.9.90

Migration steps: migrate to Sidegraph 0.14.

  • Anchors to files with the same basename keep resolving after the engine upgrade.
  • External names resolve when Graphify emits several identical stub nodes.
  • Re-importing rationale does not duplicate compatible labels shortened by a newer engine.
  • sidegraph-domains snapshot preserves domain membership across engine regrouping.
  • A store can require a minimum Sidegraph version before clients read or write it.
  • Graphify leaves installed agent skills alone when Sidegraph runs it.

Changed

  • OpenCode V2 can install Sidegraph as a native Git package with shared skills and MCP
    tools. Existing skill IDs and explicit MCP server settings take precedence. See the
    OpenCode installation guide.
  • Muse has an experimental native plugin with shared skills and MCP tools. Launch Muse
    from the intended project directory; lifecycle hooks are deferred. See the
    Muse installation guide.
  • A graph community that holds code and documents is split in two before domains are
    computed. Graphify 0.9.62 and later link documents to the code they mention, which merged
    them.
  • Domain membership is computed by each sync and kept per node. A path prefix now adds its
    files at sync time, not when a domain is read. A decision anchored under a path rule that
    the 20% cap kept off the communities now reaches that domain too.
  • A domain whose refresh fails keeps its previous membership whole and is reported again by
    every later sync until its cause is fixed; --force retries it.
  • A domain-membership pass that cannot run because another process holds the store is
    reported once, as domain membership not refreshed: the store's writer was busy, and the
    next sync runs it; --check does not count it. A pass that fails as a whole is reported
    as * (every accepted domain) until its cause is fixed.
  • The supersede_domain answer carries snapshot_note when the domain's membership is
    frozen.

Fixed

  • SessionStart and Stop no longer create an absent store. Missing host-relative stores are
    selected at the Git root; MCP first use requires explicit setup for nested or non-Git
    destinations. Init distinguishes an empty scaffold from a store with authored content
    and still prints setup guidance.

  • The MCP server applies the committed ratification policy from the selected store's
    project when no policy is set in its environment, including cross-project and symlinked
    stores. Missing settings keep the manual default.

  • Doctor excludes committed file deletions from code drift while binding statuses are
    uncomputed, and qualifies remaining changes as having unknown binding status. Projects
    without a graph still report changes to surviving anchored files.

  • Hot hooks withhold records and memory counts on a new store or after an index reload until
    sync computes binding statuses, so placeholder live rows cannot revive orphaned anchors.

  • Community names from newer Graphify builds are read from curated labels, pending hub names,
    then node names. Hub labels use the file's basename within their own community.

  • Graphify's post-merge hook marker is recognised alongside its other refresh-hook markers.

  • SessionStart and doctor advise when a graph's engine stamp is outside the tested releases.
    The advisory adds no human notice; doctor's --check exits 2 when it is present.

  • sidegraph-import recognises compatible re-cut rationale labels by their exact file/node
    context as well as the existing title/origin key, reducing duplicate imports after a
    Graphify upgrade or rollback.

  • Every Graphify run Sidegraph starts or tells you to start now sets GRAPHIFY_NO_AUTO_REFRESH=1.
    Graphify 0.9.87 and later refresh the agent skills they installed (such as
    ~/.claude/skills/graphify/) on the first run after a Graphify upgrade unless that variable is
    set. The graph refresh hook's helper, the CI recipes, the copyable commands in the docs and the
    setup skill set it, and the rebuild instructions Sidegraph prints name it. Graphify 0.9.6 does
    not read the variable.

  • Anchors to files that share a basename keep resolving on newer Graphify. Graphify 0.9.87 and
    later label such a file by a path suffix (sidegraph/cli.py) instead of its basename; Sidegraph
    reads those nodes as basenames again, so an anchor cli.py in src/sidegraph/cli.py still
    finds its node, and the engine's own label still resolves too.

  • A path-named anchor (src/sidegraph/store.py with its own file_path) now resolves to that
    file's node on any Graphify version, and is stored under the basename. Entities an earlier
    version named by the full path heal at the one-time resync that follows the upgrade.

  • find_entity reads the spelling twins of one file (an entity named by the file's path beside
    one named by its basename) as one entity, and get_entity_history lists the records bound to
    any twin. New anchors, lookups and duplicate detection compare names the same way.

  • An anchor file_path given as an absolute path under the repository, with backslashes on
    Windows, or with a leading ./ is stored repo-relative with forward slashes and binds live
    instead of orphaning, through the MCP write tools and through capture. When a store sits in a
    subdirectory of its repository, the root whose path the graph holds is the one used.

  • An anchor naming an imported external type (BaseModel, Path) now binds even when Graphify
    emits one stub node per importing file. The stubs count as one match when none has a file and
    they share a label and a node type, and the anchor gets a group binding only when they share a
    community. This applies on every Graphify version. A type and a module of one name (FastMCP
    and fastmcp) and repository code are still told apart. Document mentions of such names do
    not gain a precise leaf binding, and reading surfaces keep their earlier resolution answers.

  • Sync reports an anchor whose file disappeared and whose name now matches only external stubs
    as orphaned. A resolved external anchor without a community no longer prevents other anchors'
    previous community bindings from being retired. A formerly shared group also retires when
    the stubs split. Ambiguous anchors and failed rebinds keep those bindings when their current
    community is unknown.

Added

  • sidegraph-domains snapshot freezes every accepted domain's membership into
    .sidegraph/domain-snapshot.json, so a Graphify upgrade that regroups communities does not
    move domains. It raises the store's minimum Sidegraph version to 0.14.0. --build-with VERSION takes the snapshot from HEAD rebuilt with an older Graphify; --refresh
    re-freezes it; --dry-run prints the counts it would freeze and leaves committed files unchanged.

  • sidegraph-verify reports bad-domain-snapshot and snapshot-without-requirement;
    sidegraph-doctor reports snapshot-split; sidegraph-sync --json and sync_anchors
    carry snapshot_splits.

  • sidegraph-require --sidegraph X.Y.Z makes a store require at least that Sidegraph release:
    the committed marker is raised atomically and never lowered. New clients check it before
    using the store; verification reports malformed requirements and requirements above its version.

  • sidegraph-doctor prints informational spelling twins lines for an entity named by a path of
    its file beside one named by the basename. They are not findings and do not change the exit
    code. duplicate-entity no longer reports such a pair.

  • A new advisory check, refresh-helper-outdated, reports a graph refresh hook whose helper runs
    Graphify without GRAPHIFY_NO_AUTO_REFRESH=1 (the helper an earlier sidegraph-init wrote). It
    shows as a SessionStart line and notice and as a sidegraph-doctor finding, and
    sidegraph-doctor --check exits 2 on it. Run sidegraph-init with no flag: it rewrites the
    helper and keeps your recorded hook choice. The helper lives in .git/hooks, so each clone
    runs it once.

Sidegraph 0.14.0 RC1

Sidegraph 0.14.0 RC1 Pre-release
Pre-release

Choose a tag to compare

@SantyagoSeaman SantyagoSeaman released this 16 Oct 17:31

Sidegraph 0.14.0 — release candidate 1

Opt-in Git candidate for testing real host installation and memory workflows. This is a
GitHub prerelease, not the stable release and not uploaded to PyPI/npm. Package metadata
is 0.14.0; the immutable candidate identity is Git tag rc/0.14.0-1. Stable main stays
on 0.13.0. Final Windows/macOS native acceptance is still in progress.

The main changes preserve anchors and domain membership across the Graphify 0.9.90 upgrade,
fix store-setup/ratification edge cases, and add native Git packaging for OpenCode V2 plus
experimental Muse MCP/shared skills. Use the RC-specific commands below; the README describes the normal stable installation.
Use a separate host profile and test repository for
this candidate; the sidegraph@sidegraph identity is shared with stable. An existing
marketplace registered to main must not be assumed to switch refs on a repeat add.

Existing stores: follow the 0.14 migration guide
before upgrading Graphify. Update every Sidegraph client first. Back up the store and
follow that guide's domain snapshot steps rather than upgrading Graphify first.

Claude Code

In a fresh test profile, run in your terminal:

claude plugin marketplace add 'https://github.com/SantyagoSeaman/sidegraph.git#rc/0.14.0-1'
claude plugin install sidegraph@sidegraph

Review installation/approval prompts and start a new session in your test project.
Tagged Git marketplace syntax: official command reference.

Codex

In a fresh test profile:

codex plugin marketplace add SantyagoSeaman/sidegraph --ref rc/0.14.0-1
codex plugin add sidegraph@sidegraph

Start a new interactive session in the test project. Review and approve Sidegraph hooks
through the startup warning or /hooks. Installation alone does not grant hook trust.

OpenCode V2

Requires OpenCode V2 (baseline 2.0.29), uv and Git. In a fresh test profile:

opencode plugin add 'git+https://github.com/SantyagoSeaman/sidegraph.git#rc/0.14.0-1'

Restart OpenCode in the test project, keep normal MCP/skill permissions and use
@sidegraph-setup. MCP and skills only; no automatic session map/read injection/Stop nudge.

Muse (experimental)

Basic MCP/shared skills only; expanded hooks/workspace/Windows acceptance are deferred.
From a stable directory outside your project:

git clone -c core.autocrlf=false --branch rc/0.14.0-1 --single-branch https://github.com/SantyagoSeaman/sidegraph.git sidegraph-rc
muse plugins install ./sidegraph-rc/plugin/sidegraph
muse plugins inspect sidegraph

Review the MCP capability, then:

muse plugins approve sidegraph

Start a new Muse process from the intended test project; restart when switching projects.
Basic native evidence covers Muse 1.4.7/macOS; no new Muse parity claim.

Verify memory

Initialize the test project using this candidate:

uvx --from git+https://github.com/SantyagoSeaman/sidegraph.git@rc/0.14.0-1 sidegraph-init

Ask the agent to save a real gotcha anchored to a real project file, then in a fresh
session ask get_task_context for that file and confirm it recalls the gotcha. OpenCode
and Muse need these explicit MCP requests. Record install errors and actual host/runtime
versions when reporting a candidate issue; remove credentials and private content.

The tag is immutable: a correction gets a new RC tag rather than changing this one.
To return to stable, leave the test profile and use your unchanged normal profile. Stable
plugin caches and the optional PATH CLI have separate update commands in the README.

v0.13.0

Choose a tag to compare

@SantyagoSeaman SantyagoSeaman released this 14 Oct 07:46

[0.13.0] — 2026-10-14

Fixed

  • On Windows, hooks misread the payload the host sends when a path in it has a non-ASCII
    character (an accented or Cyrillic user name or project folder). The Stop capture reminder
    never fired, PreToolUse delivered no records, and for some names SessionStart and
    SubagentStart printed nothing.
  • On Windows, a non-ASCII git user name, branch name or repository path was read back garbled:
    a ratified record could carry a garbled name in its provenance, and sidegraph-verify --against failed or reported false changes for records containing non-ASCII text.
  • A change to a file whose name has non-ASCII characters, spaces or quotes was missed by the
    drift check and the commit-trailer candidates, because git escaped the name.
  • On Windows, a Codex hook started from a subdirectory of a repository whose path has non-ASCII
    characters now runs from the repository root.
  • A default Git for Windows install no longer breaks the MCP server. Its installer puts only
    Git\cmd on PATH, which has no sh, so the server, which started with sh, did not start.
    The Codex hooks have a Windows command for the same reason (see Changed).
  • The project's ratify policy now applies under Codex. sidegraph-init commits it to
    .claude/settings.json, which Codex does not read, so under Codex every agent proposal
    waited for review whatever the team chose. The plugin's MCP server now reads the committed
    value when its environment sets none; an explicit SIDEGRAPH_RATIFY_POLICY still wins.

Changed

  • The plugins launch without sh on PATH. Both MCP launchers start uvx (uv in the
    development manifest) directly, and sidegraph-mcp --repo-root anchors the server to the
    project: CLAUDE_PROJECT_DIR, else the git top-level, else the working directory. The Codex
    hooks gain a PowerShell commandWindows that Windows PowerShell 5.1 parses. On native Windows
    you need no PATH change and no PowerShell 7; Git for Windows at its default options is
    enough. The Codex hooks need a Codex release that supports commandWindows (verified with
    0.162.1). The Windows hook definition changes, so approve the changed hooks once in /hooks
    on Windows; other platforms are unaffected.
  • The optional terminal CLI now installs from the same place as the plugin, the public
    repository's main branch: uv tool install git+https://github.com/SantyagoSeaman/sidegraph.git@main,
    updated after each release with uv tool upgrade sidegraph. The docs no longer send you to
    PyPI for it; PyPI still carries every release for pinning an exact version. The path-skew
    SessionStart line now offers uv tool install --reinstall git+https://github.com/SantyagoSeaman/sidegraph.git@main,
    which moves an older install from PyPI, pinned or not, to the latest release.
  • README: the Quickstart is one numbered sequence with the plugin commands inline, a teammate
    path, and the rarer options below it; "How it works" is a numbered list instead of a diagram.

v0.12.0

Choose a tag to compare

@SantyagoSeaman SantyagoSeaman released this 12 Oct 20:32

[0.12.0] — 2026-10-12

Fixed

  • The Codex plugin's SessionStart hook now starts on native Windows. Its command used POSIX
    syntax (VAR=value command, $(...)) that the PowerShell Codex runs hooks in cannot parse, so
    Windows sessions showed "the SessionStart hook could not start" although the entry point itself
    worked when run directly. It is now wrapped in sh -c, like the Stop and SubagentStart
    hooks, and keeps the same message if the launch fails. Codex records trust per hook definition,
    so approve the changed SessionStart hook once in /hooks. On Windows the Codex hooks need
    PowerShell 7: Codex runs hooks in PowerShell, and Windows PowerShell 5.1 cannot run them.
  • On native Windows, sidegraph-import now imports the documents a profile covers: their
    repo-relative paths were built with backslashes, so every in-repo document was counted as
    outside the profile and nothing was imported. Record keys, anchor lookups and provenance.ref
    now use the POSIX path Graphify's source_file carries, so a later re-import on another
    platform does not duplicate records going forward, and a symlink that resolves outside the
    repository is refused there as on other platforms. A document on another drive than the
    repository no longer crashes the import. A store that already holds backslash-keyed records
    from an earlier Windows --any-doc import gets a second, POSIX-keyed record for the same
    document on re-import; supersede the old ones.
  • On native Windows, records are now delivered before a Read, Edit, Write or Grep, and before a
    Bash cat, sed, grep or rg, and a subagent brief's files find their records. These paths
    were built with backslashes (src\sample.py) and never matched the POSIX paths anchors use.
    Touch events and the usage statistics now record POSIX paths, and query_decisions(files=…)
    without a graph finds the file's records. A root-level file whose name starts with .. is no
    longer mistaken for a path outside the repository. Hooks now key a Windows session by its
    transcript file name, as on other platforms, instead of the whole C:\… path, so hook showings,
    touch events and capture state join the session the MCP server attributes calls to.
  • The 40,000-character hard cap on get_task_context and query_decisions now keeps its advice
    blocks (how the seeds were read, what is not in the graph, the supersede hint) when a record's
    text contains a ## line or a seed path is absurdly long, and the closing note names what was
    cut. Seed paths echoed in those blocks are clipped to their head and file name. The usage
    statistics count only the records left in the text after the cap, nearest-anchored records
    included, and a worktree seed written as an absolute or nested path is journaled as the
    repo-relative path the answer shows. A record spanning several lines is cut whole, never
    without its id line, and degraded and chars_used describe the delivered text too. The
    bound-record count of the nearest-anchored block reads "at least N" for a path whose bound
    records include some of the nearest records shown.
  • drill_down bounds the whole answer, not only the decisions: a domain summary, titles and member
    lines too long for budget_chars (measured as serialized JSON, a slug included) are clipped with a
    marker, and subdomains or members that do not fit are left out with subdomains_omitted /
    members_omitted counts and a hint.
  • sidegraph-doctor no longer crashes on an index.db that has anchor_bindings but no meta
    table: such an index counts as having its binding statuses computed, so the checks run. Only an
    unreadable index is reported as skipped.
  • sidegraph-doctor no longer reports zero degraded-binding/orphaned-binding findings while
    binding statuses are not computed for the current graph (after an index reload and before the
    next sidegraph-sync). It lists binding-status among the skipped checks, and the text
    line for it and orphaned-records no longer blames a missing index.db.
  • get_task_context no longer calls a real file of a sibling linked worktree "not a
    repo-relative path", guesses it to be a same-named graph file, or reads a nested worktree path
    as the main checkout's file. It names the worktree and branch; a well-formed path that is a file in no checkout is "not a
    file in this checkout", and only malformed paths keep the old sentence. A path with records
    bound to a file the graph lacks and not shown elsewhere in the answer says so in the
    nearest-anchored block, and
    retrieve_decisions(files=[...]) reads such a path exactly.
  • Attribute each MCP call to its own session instead of the last SessionStart of any host
    in the repository. A write that omits session_id and a retrieval-journal row now carry the
    calling host's session: CLAUDE_CODE_SESSION_ID for Claude Code (2.1.154 or later for exact
    attribution, 2.1.163 or later to keep one id across --resume) and the request threadId
    for Codex. A client that names its host but no session falls back to the one fresh stamp of
    the same host family, else to nothing; a retrieval that cannot be attributed is journaled as
    unattributed and sidegraph-stats reports those calls instead of counting their sessions
    as silent. The sidegraph-mcp console script moves to sidegraph.host.mcp_entry:main
    (the command name is unchanged; python -m sidegraph.server still starts the server, without
    host identity). Hook commands set the new SIDEGRAPH_HOST variable. Hand-written
    SessionStart wiring should add SIDEGRAPH_HOST=claude or SIDEGRAPH_HOST=codex.
  • Serialize Store state checks and publication behind a SQLite writer reservation,
    including pending verdicts and server-side domain/fact preconditions. Busy admission
    reports conservative inspect-before-retry guidance; file and index commits remain
    separate operations.
  • Read graph/label JSON and Claude settings as UTF-8 independently of the default locale;
    retry incomplete graph encodings and close temporary descriptors on setup failures.
  • Isolate noninteractive Git subprocess stdin from the MCP protocol pipe, resolving
    native Windows first-write stalls in the installed-wheel stdio checks.
  • Set MCP isError for complete anchor-operation failures while retaining structured
    payloads and partial-batch outcomes. Unknown tool names receive bounded discovery advice.

Changed

  • The Quickstart no longer installs the CLI for plugin users: its commands run through uvx,
    as the plugin does. uv tool install sidegraph is documented as optional beside a plugin
    and the usual setup without one, and its install now includes a mandatory update after every
    release, uv tool install sidegraph@latest. The plugin's skills run every
    sidegraph-* command at the session's version through uvx and never the copy on PATH.
  • The PreToolUse hook records what it hands over as showings: each record printed in a
    file-read block (inject) or appended to a subagent's brief (inject_brief) is journaled
    and bumps retrieval_shows. sidegraph-stats gains a record showings delivered by hooks
    line (activation.hook_showings in --json), and no recorded showing and the doctor's
    never-surfaced check count a record the hook printed as shown. Upgrade sidegraph-stats
    together with the plugin: an older CLI reads the new journal rows as memory lookups.
    The Agent-brief path, read-only until now, writes one transaction (up to the one-second busy
    timeout under a held lock; a failed write never costs the brief).
  • Bound the output of five listing tools. list_facts, get_entity_history, drill_down,
    list_domain_candidates and list_proposed now take budget_chars (default 24000,
    maximum 40000) and say what was left out and how to narrow; list_facts gains status,
    files, query, ids and limit, and get_entity_history gains status and limit.
    Before, a store with a few hundred facts or a long-lived entity produced answers (up to
    170,000 characters) that Claude Code replaced with an "exceeds maximum allowed tokens"
    notice. Return shapes change: list_facts and get_entity_history return a JSON
    object instead of a list; drill_down and list_domain_candidates add
    hint/truncated-style fields (omitted_ids, omitted_by_budget); the first four are sent
    once, as text, with no structuredContent copy. list_domain_candidates shows fewer
    candidates by default (the budget cuts first; narrow with paths; limit=0 still lifts the
    count limit). retrieve_decisions's maximum budget_chars falls from 60000 to 40000, below
    Claude Code's 50,000-character limit, and get_task_context, query_structure,
    query_decisions and list_proposed never return more than 40,000 characters (a cut says
    so in a closing note and keeps the advice blocks).
  • Text-only MCP tools get_task_context, query_structure, query_decisions, and
    list_proposed no longer advertise an output schema or duplicate text in
    structuredContent. Machine-oriented dictionary results retain their schemas.
  • Bound supported dependency majors to FastMCP 3 (>=3.4.2) and MCP Python SDK 1 (>=1.24).

Added

  • A SessionStart advisory, path-skew, on macOS and Linux, when a sidegraph-* command on
    PATH (outside the session's own environment) is a different version than the session, such
    as a forgotten uv tool install sidegraph. It reads the install's dist-info and runs nothing.
  • Per-process MCP tool admission: burst 200, refill 100 calls/second, immediate refusal
    before tool work; initialization, ping, and discovery remain available.
  • Native Windows x64 CI for Python 3.13/3.14, covering selected core/MCP contracts and
    an exact installed-wheel CLI/stdio probe outside the checkout.

Documentation

  • Reorganize the README around setup, usage, limits, and contribution paths; remove
    unsupp...
Read more

0.11.1 — Security patch

Choose a tag to compare

@SantyagoSeaman SantyagoSeaman released this 10 Oct 19:01

Security patch for Sidegraph 0.11.0. Users of 0.11.0 should upgrade to 0.11.1.

Published on 2026-10-10.

Fixed

  • Bootstrap excludes special files and validates one bounded regular descriptor. FIFO,
    sockets and devices cannot become selected text sources, including through --include.
    Available nonblocking/no-follow flags guard final-component replacement; the opened
    descriptor is rechecked before strict incremental UTF-8 validation. Directory aliases
    are rechecked against exclusions. Include retains its size override but observed growth
    past opened size is excluded. Later planner/apply reads and ancestor races retain their
    existing scope.

  • Git verification preserves unusual filenames through binary NUL framing. Verify and
    doctor detect immutable edits and archive deletions at paths containing Unicode, TAB,
    LF, CR, quotes, backslashes or surrounding whitespace. Malformed or unsupported diff
    output is an operational error (exit 1), never a partial clean report. Shared Git
    callers keep their decoded-text behavior; only diff reads raw pathname bytes.

  • Git verification resolves --against to one protected, frozen commit ID. Verify and
    doctor reject option-shaped, empty, NUL-containing, unavailable and non-commit baselines
    operationally (exit 1). Diff and every baseline record read use the same full SHA-1 or
    SHA-256 ID, so a branch moving between those operations cannot hide an immutable edit.
    There is no raw-ref fallback. Current working-tree reads retain
    their existing concurrency limits.

  • Malformed write diagnostics omit raw input and arbitrary failure causes. Capture
    errors use bounded schema fields/codes; logical MCP writes apply raw admission before
    signature coercion and intercept exceptions before framework warning/error logging.
    Unknown write arguments keep valid parameter help without echoing their names.
    Unsupported registrations fail closed. Partial-write warnings and recovery guidance
    remain truthful; domain/ratify causes are controlled. Read diagnostics and upstream
    FastMCP DEBUG argument tracing retain their existing scope.

  • Capture and direct writes reject oversized input before redaction or canonical writes.
    Limits are 256 KiB UTF-8 per string, 4 MiB admitted strings and 65,536 admitted nodes per
    request, depth 32 and 4,096 nodes per draft, and 100 agent drafts including attached facts.
    Combined decisions/facts share one budget; item violations reject the containing draft,
    while request-wide violations fail before any write. Documents have an independent 8 MiB
    per-file limit with whole-file skips, matching real/dry reports and no total-import cap.
    Import tags and free-text options are bounded separately; input is never silently truncated.
    Raw Python batches use lists/tuples; supported scalar, mapping and model fields are
    inspected. Other opaque Python values (including sets, deques, generators and non-string
    Enum objects) are rejected before Pydantic coercion, without consuming iterators. Numeric
    enums can become strings rather than behave as numbers in Pydantic string fields; only
    string-derived enums keep their measured base-string behavior.

  • Redaction avoids repeated suffix searches in four known attack families. Linear
    assignment-key, URL-credential, JWT and private-key-block recognizers preserve matching
    order/counts and the existing conservative quoted-value behavior. This does not claim
    universal regex linearity or complete secret detection. See security policy.

  • Secret-bearing tags are redacted before document import stores their slugs. Capture,
    direct MCP writes and document import share tag normalization. Import reports tag
    substitutions once per batch in real and dry runs, including duplicate raw tags.
    Comma-separated MCP strings are cleaned before splitting; a secret-bearing string with
    commas drops all its tags conservatively. List input keeps explicit boundaries.

  • Quoted assignment redaction consumes multiline, triple-quoted and unterminated values.
    Escaped closers and glued tails stay covered. An unterminated value removes the
    remaining input conservatively if no legal closer is found. An assignment span,
    including URL/card expansion, that would swallow another assignment's key also consumes
    the remaining input and following prose. A multiline quoted span splitting a URL
    credential or checksum-valid grouped card number does so too. Bare values and
    single-line glued tails extend through crossing URL/card matches while otherwise
    preserving later prose. Prior tag entities and Git history remain: review old tags and
    rotate exposed credentials. Unicode letters/digits after a single quote keep it inside
    the value as an apostrophe; an unclosed value removes the remainder.

  • Secret scanning covers store-only commits and committed CI history. The pinned
    Gitleaks hook runs even when changed files are excluded from formatting. A separate manual
    gitleaks-history hook scans committed HEAD history, also run by CI with a full checkout.
    Scanner integration tests exercise staged, committed, and removed synthetic secrets.

  • Store verification checks the content of newly archived history. A matching archive ID
    alone no longer permits deletion of a committed hot decision or domain: every archive copy
    must be compactable and a legal transition from the baseline, preserving raw immutable keys.
    The snapshot check also reports nonterminal archive records. Legal supersede/drop followed
    by compaction, older absent null/empty defaults, identical merge copies, and crash cleanup
    still pass.

Changed

  • Security documentation states the limits explicitly. The privacy overview describes
    best-effort redaction and writes outside the store. CLI documentation distinguishes
    argument syntax errors from rejected Git baselines and recommends JSON output when
    a terminal cannot encode a reported pathname.

v0.11.0

Choose a tag to compare

@SantyagoSeaman SantyagoSeaman released this 09 Oct 11:11

[0.11.0] — 2026-10-09

Changed

  • index.db no longer creates and deletes its journal in every write transaction. SQLite's
    default rollback mode (DELETE) created index.db-journal at the start of each write and
    removed it at the end, and the hooks write on every tool call, so one store produced
    hundreds of thousands of file-system events an hour. Every connection that writes now runs
    journal_mode=TRUNCATE: the same rollback journal, locking and crash safety, but the journal
    is emptied rather than deleted, so a 0-byte index.db-journal stays beside index.db
    (gitignored by index.db*, as before). Each transaction still writes, truncates and syncs
    that file, so the events do not vanish: they fall most for back-to-back writes (tests, bulk
    imports: about nine times fewer on the journal) and by about a third for hook traffic.
    Read-only connections still write nothing, and there is normally no -wal/-shm; an index
    that another connection has pinned in WAL stays in WAL, which Sidegraph tolerates. A crash
    mid-transaction still leaves a non-empty journal that the next writer rolls back.

Fixed

  • The UNANCHORED line of sidegraph-bootstrap now names the graph in use. It always said
    to run graphify update ., which rebuilds only the default graph, so with --graph or
    $SIDEGRAPH_GRAPH elsewhere the advice could not make the next run write the document. It now
    uses the same step as the ANCHORS line: rebuild the graph at <path> and rerun for a custom
    graph, run graphify update . and rerun for the default one.
  • A fresh sidegraph-bootstrap run now lists the store's creation marker as changed. The
    CHANGED lines and --report named format and .gitignore but not the committed
    stamping_live_since file the same run created.
  • sidegraph-bootstrap now says what is wrong when a ratified or revived record is out of the
    graph's reach.
    For an accepted candidate with no anchor in the current graph whose content
    is already stored (ratified in place, or revived from a rejected record), every rerun used to
    print ANCHORS incomplete: accepted candidate has unresolved or ambiguous anchors,
    PROOF incomplete: selected decision did not surface and a RESUME line that could never
    succeed, and exit 2. The anchors are now judged by whether the
    record still surfaces through production retrieval, the same test as PROOF: when it does
    not, the run still exits 2 but prints ``ANCHORS incomplete: stored record for has no anchor in the current graph; run `graphify update .` and rerun``, a matching `PROOF` reason and a `NEXT` line with that command instead of `RESUME`; once the graph has the node, the rerun exits 0. The exact `RESUME` stays when something a resume can fix (a failed write, missing host wiring) is also wrong, and with `--graph` the line names the graph to rebuild instead. The `--report` file carries the same remedy. A written record with an unresolved backticked mention keeps its old message.
  • A directory named like a record no longer makes the whole store unopenable. A directory
    (or a dangling or looping symlink) called .x.json, or 01ABC.json, in decisions/,
    facts/, domains/, entities/ or initiatives/ (or in bindings/ under a safe name, or
    archive/ as *.jsonl) crashed every open of the store, so every hook and MCP call failed
    (IsADirectoryError for a directory, Too many levels of symbolic links for a looping
    link), and a FIFO there blocked the open for ever. The reload now skips any entry that is not
    a regular file before reading it, lists it in the skipped files with the reason not a regular file, and the startup warning names it; the other records load and the next open
    takes the fast path. sidegraph-verify reports such an entry once, as unsafe-record-id
    for an unsafe record name and as parse-error (bad-archive-segment in archive/,
    whatever the segment's name) otherwise, and no longer blocks on a FIFO, including
    --against when a tracked file was replaced by one. A stale directory or looping symlink
    named *.tmp no longer fails the open either. sidegraph-stats no longer ends in a
    traceback on a looping link, and no longer reports the index as behind for ever because of a
    dangling symlink. The skip reason for a file with an unsafe name now says unsafe filename
    instead of "id 'A' does not match the filename".
  • sidegraph-prepare-commit-msg no longer leaves its candidate block in git commit -m
    messages under the pre-commit framework.
    The framework passes the hook only the message
    file and hands git's source in PRE_COMMIT_COMMIT_MSG_SOURCE, so the hook always saw "source
    absent" and wrote its # Sidegraph: uncomment the trailers… and # Sidegraph-Decision: …
    lines, which -m commits keep. With exactly one argument the hook now reads the source from
    that variable (unset or empty means a plain commit); with two or three arguments, as raw git
    passes them, the arguments still win.
  • The hook entry points answer --help instead of running. sidegraph-session-start --help
    ran the SessionStart hook in the current directory and wrote into its index.db, and the other
    three hook scripts (sidegraph-stop, sidegraph-pre-tool-use, sidegraph-subagent-start)
    ignored their arguments the same way, while sidegraph-prepare-commit-msg --help printed
    nothing and sidegraph-mcp --help started the server. Each now prints a short usage text for
    -h or --help and exits 0 before it reads stdin, opens the store or writes anything. Any
    other argument to a hook script is a usage error on stderr with exit 2. The git hook
    sidegraph-prepare-commit-msg stays fail-open: it prints the same error for an unknown option
    or a fourth argument but exits 0 and leaves the message untouched, so git commit never fails
    because of a stray args: entry; git's one to three positional arguments are unchanged. Hosts call the hook scripts without
    arguments, so nothing a host runs changes.
  • sidegraph-bootstrap now converges when an accepted candidate has no anchor. A document
    with no backticked symbol and no node in the graph (typically one newer than the last graph
    build) was never written, yet stayed pending: every run, --resume included, ended
    partial-recoverable or incomplete with exit 2 and a FAILED REF <none> the operator
    could not act on. Review now flags it with a no-anchor warning, and apply skips it by name
    on an UNANCHORED line with its source path (skipped_unanchorable_keys in the report object)
    and counts it as neither durable nor pending, so it no longer keeps the run partial. A run
    where every approved candidate is skipped this way writes no record and ends as a diagnostic
    that exits 0. It keeps the checks a run with records has, except the anchor and proof ones:
    the store reopens and verifies clean, the host integration is verified, and --report is
    written. A missing host integration or a store violation exits 2 instead (STORE incomplete, with RESUME, or sidegraph-verify for a violation), as does an anchored
    candidate kept proposed. Run graphify update . (or rebuild the --graph file) and the next
    run writes it.
    sidegraph-import is unchanged.
  • sidegraph-verify now reports a domain whose parent_id is not a domain, or whose parent
    chain loops.
    The store refuses to write either, but a hand-edited domain with
    parent_id: "not-a-domain" passed sidegraph-verify and sidegraph-doctor with exit 0. A
    parent_id that names no domain, hot or archived, is now a dangling-parent, and a parent
    chain that comes back to itself (a domain that is its own parent included) is one
    parent-cycle per cycle, at the domain with the smallest id on it. A superseded, dropped or
    compacted parent still counts as existing, so a history written through the tools stays
    clean, and a parent held by a file skipped for its name is not reported twice. A non-string
    parent_id stays a parse-error (a bad-archive-segment in an archive segment).
  • A record directory the process cannot list or search is reported as unreadable, not as
    missing records.
    With decisions/ at mode 000, sidegraph-verify printed a
    dangling-fact-support for every fact and --against an illegal-deletion for every
    record, as if the store had been tampered with. sidegraph-verify and sidegraph-doctor now
    stop with one store not readable line naming the directory and exit 1, before any
    cross-reference or deletion check runs. sidegraph-stats likewise says cannot read the store index instead of no store index when the store directory cannot be searched, and no
    longer ends in a traceback when a record directory cannot be listed. An empty record
    directory without the search bit is reported the same way.
  • sidegraph-verify reports only unsafe-record-id for a record file the store skips for
    its name or id.
    A copy such as decisions/.x.json was also reported as
    filename-id-mismatch and duplicate-ulid, and a file with a safe name but an unsafe id as
    filename-id-mismatch too, though the store never indexes either. A file with an unsafe
    name that was not valid JSON, not UTF-8 or not an object showed only parse-error, and the
    same went for a bindings/ file that was not a JSON list, so the real problem, the name,
    went unreported. The name is now checked before the content, in every record directory. A
    file skipped for its name no longer counts as a second copy of a record, but a record that
    points at its id is not reported either: renaming the file back is the only fix.
  • sidegraph-compact no longer warns "DIFFERENT content" for a hot file that only lacks
    newer keys.
    A decision or domain written before ratified_at, ratified_by or
    provenance.commit existed has no such keys, while...
Read more

v0.10.0

Choose a tag to compare

@SantyagoSeaman SantyagoSeaman released this 04 Oct 16:31

[0.10.0] — 2026-10-04

Added

  • Every MCP tool now carries annotations, so a host's auto-reviewer stops rejecting memory
    reads.
    In the field, Codex's auto-reviewer rejected 20 get_task_context calls, and each
    one lost memory for that step: an unannotated tool takes the MCP defaults (destructive,
    open-world), and the review's verdict then depends on the prompt. The seven tools that never
    change a tracked file are read-only; the other 17 are non-destructive, closed-world local
    writes. No tool sends anything off the machine and none deletes a record. A test runs the
    read-only tools on a store where a lazy sync would adopt a moved symbol, and fails if one of
    them changes a tracked file, so the annotation cannot drift from what the tool does. See the
    reference and the
    Codex page.
  • The seed tools accept the argument names agents try, and every tool names its parameters in
    an error.
    get_task_context, query_structure and query_decisions accept paths (as
    files) and seeds; each seed goes to files when it looks like a path and to entities
    otherwise. task is refused with a line saying there is no free-text query. Any other
    unknown argument, on any tool, is an error that names it and lists the tool's real parameters;
    FastMCP's own error named the argument but not the parameters.

Changed

  • retrieve_decisions no longer returns the whole store. One call returned a full dump of
    every visible decision: about 143,000 tokens on a store of 260 records, more than a host would
    show, and in a field report most agents passed include_superseded: true and then either
    printed the dump or filtered it by hand. The call now has two answers.
    • With no narrowing filter it returns an overview: the total, counts by status and kind
      (superseded and rejected records included, so the counts say that history exists), and the
      newest records as compact rows (id, kind, status, title, valid_from).
    • With a narrowing filter it returns full records, gotchas and lessons first and newest
      first, cut at limit (default 25, at most 100) or budget_chars (default 24,000, at most
      60,000), whichever comes first. A hint says what cut the list, or why it is empty.
    • The filters are new arguments, and they combine with AND: status and kind (a string or a
      list), files, query (every word must occur in the record's text) and ids (exact record
      ids, superseded ones too). include_superseded keeps its meaning: it widens the overview's
      newest and a filtered call that has no status.
    • files are read the way get_task_context reads them: a basename, a suffix, a directory or
      a letter-case slip works. unresolved_files lists, for each path nothing could read (at most
      ten, with unresolved_omitted counting the rest), why (ambiguous, directory-too-large,
      not-in-graph or no-graph) and the candidates to pass instead.
    • The return type changed from a list to a JSON object, sent once as a single text block
      with no structured copy
      , so budget_chars is the real cost on every host. A client that
      read the list now reads decisions of a filtered answer. The proposal policy is unchanged
      and applies to every shape.
    • The check-plan, explain-why, triage-drift and heal-anchors skills now call it with a
      filter, and a test fails if a skill calls it without one. See the
      reference.

Fixed

  • A fault converting a reviewed item in sidegraph-bootstrap now prints the RESUME line. The
    conversion of each reviewed item, and the check that its ref still matches the candidate,
    ran outside the per-item error handling. A fault there, after an earlier item had been
    written, left apply_review without a report: an AssertionError ended the run as a
    traceback, a ValueError was reported as a failure before anything was written, and neither
    printed RESUME, though canonical files were already on disk. The fault now takes the path a
    write error takes: the run stops, reports partial-recoverable with the durable candidates and
    the failing ref, and prints the resume command. A fault on the first item, before anything is
    written, now exits 2 with STORE incomplete, the failing ref and the resume command, where a
    ValueError used to exit 1 with an ERROR line; the terminal shows the failing ref and not the
    reason.
  • sidegraph-import --dry-run now says what auto-ratification would do. Both import paths
    decided auto-ratification only after a real write, so under a non-manual
    SIDEGRAPH_RATIFY_POLICY a dry run could not show which records a real run would land
    accepted without a human: on one fixture it reported 0 where the real run auto-ratified 2. The
    dry run now runs the same eligibility gate on the record the write would produce and reports
    the result in a new would_auto_ratify field on ImportReport and DocImportReport, and as
    auto_ratify_eligible on each dry-run item. auto_ratified keeps its meaning, transitions
    that actually happened, so it stays 0 in a dry run.
    • Behaviour change: under a non-manual policy the summary line gains would auto-ratify M before (skipped: and an eligible item line ends with [auto]. Under
      manual both are unchanged. A dry run still never carries the real run's auto-ratified K.
    • The count is eligibility, not a guarantee: a real run's transition can still fail, for
      example on a race or a refused cascade.
    • A key repeated within one run no longer counts twice in the dry run. Two rationales with the
      same title in one file count once. A document ref met more than once (one file named twice, a
      directory plus a file inside it, or a live and an archived copy that a profile maps to one
      ref) is classified against the records the earlier copies would have left there, as the real
      run sees them: an identical repeat is skipped, a differing one supersedes, and a copy after a
      rejected first copy is a fresh write. Two rare shapes can still differ in the
      superseded/skipped split, not in the eligibility count where checked: a ref met three
      times with the first content returning, under an auto policy, and a repeat with no anchor of
      its own.
  • find_entity no longer calls a dangling record a decision. Each binding was labelled
    fact if a fact lookup found it and decision otherwise, so a binding whose record exists in
    neither table (a record file removed by hand, or lost in a merge) came back as a decision that
    get_decision could not fetch, and each binding cost a query of its own. The types of all of an
    entity's bindings now come from one read of the index, and a binding with no record reports
    record_type: "unknown". The value is additive: a client that handles decision and fact
    is unaffected. get_entity_history uses the same read and still skips such a binding. See the
    reference.
  • A document status like should not be rejected no longer lands rejected. The check for
    a turned-down status saw a negation only as not and one space directly before the word, so
    should not be rejected, not rejected (two spaces), never rejected and isn't rejected
    all read as rejections, and a false rejected lands closed and invisible to retrieval. A
    negation is now not, never or a word ending n't within the three words before the word in
    the same clause. A clause ends at , ; . :, a pipe, a slash, an em or en dash, or a spaced
    hyphen or ->, so not reviewed, rejected, Not accepted — rejected and Won't do / rejected
    still read as rejections, as does not rejected, rejected. A conjunction does not end a
    clause: Not adopted and rejected reads as negated, and lands accepted unless --propose or
    a draft marker sends it to the ratification queue. See the
    reference.

v0.9.0

Choose a tag to compare

@SantyagoSeaman SantyagoSeaman released this 04 Oct 06:09

[0.9.0] — 2026-10-04

Added

  • Host wiring that silently switched memory off is now visible. Two field failures went
    unnoticed for weeks: hooks that ran an old install under a newer plugin, and sessions started in
    a subdirectory where the plugin was off. SessionStart now prints Sidegraph <version> right
    after the standing instruction, and two advisory checks report the wiring, each with a notice
    at most once a day (plugin-off-in-subdirectories only when a nested directory's own settings
    leave the plugin off).

    • version-skew compares the running package with the plugin manifest under
      $CLAUDE_PLUGIN_ROOT (or else $PLUGIN_ROOT, which Codex sets) by numeric parts, and says
      which side is stale. It does not run without a plugin root, a manifest named sidegraph, or a
      version.
    • plugin-off-in-subdirectories applies Claude Code's settings model (a launch directory's own
      project settings only, the repository root's .claude/settings.local.json for every launch,
      enabledPlugins merged key by key over user settings) to the directories of the repository.
      It reports a plugin enabled only in the root's .claude/settings.json, and nested directories
      whose own enabledPlugins leave it off. User settings are read from
      $CLAUDE_CONFIG_DIR/settings.json when that variable is set, else from ~/.claude/settings.json. It also lists in sidegraph-doctor under the finding code
      plugin-off-in-subdirectories, and --check exits 2 on it like any finding.
      The getting-started pages now say that enabling the plugin at user scope, or in the root's
      .claude/settings.local.json, covers every subdirectory, and the setup skill reads the doctor
      finding. See troubleshooting.
  • A subagent's brief carries the records for the files it names. The task a subagent works
    on is in the brief its parent wrote, which the SubagentStart brief cannot see. The
    sidegraph-pre-tool-use entry point now also handles Agent (and Task, the older name): it
    reads the files the brief names, and the files named in a .md, .txt or .rst document the
    brief names (one hop, the first 64 KB), finds the records anchored to them, and returns the
    call's own input with a block appended to prompt (updatedInput; no permissionDecision).
    The subagent finds the block at the end of its first message. The parent's view of its own
    call does not show it
    : its transcript keeps the prompt it wrote. A path resolves exactly when
    it is an existing regular file inside the project root, and otherwise by a unique suffix among
    the anchored files (sub/x.py for pkg/sub/x.py; an ambiguous suffix names nothing), tried
    from the directory the agent was launched in before the root. A document is followed only when
    it is a regular file inside the root, and no hop goes through a project-instruction file
    (CLAUDE.md, AGENTS.md and their .public twins): they are still shown when records are
    anchored to them, but the files they list are not followed. The block is grouped by file, two
    records each, at most six record lines and 3,000 characters, and it ends with a
    More: get_task_context(files=[…]) line for the files shown; a file that misses the sixth
    line by one still gets its first record there. It is on by default;
    SIDEGRAPH_AGENT_BRIEF=off disables it. The plugin manifests (Claude Code only, the Codex ones
    are unchanged) and the manual recipe gain a third PreToolUse group, matcher Agent|Task, on
    the same command; a hand-wired setup needs that group added. Not tested: agent-team teammates
    and fork spawns. See the
    hooks reference.

  • Every subagent starts knowing that memory exists and how to ask it. A subagent gets no
    SessionStart context, and Explore and Plan agents load no CLAUDE.md either, so nothing
    standing told one that decision memory exists. The new sidegraph-subagent-start hook, wired
    to SubagentStart in the Claude Code and Codex plugins, answers with a short brief: the same
    standing get_task_context(files=[…]) call SessionStart gives the main agent, and one
    sentence of what memory holds ("N records anchored to code in M files, K of them recorded
    mistakes"). The counts are what get_task_context could return: live records that may surface,
    with a live or degraded binding to an entity that names a file. It carries no records, prints
    {} where there is nothing anchored to code, and, like PreToolUse, reads the index directly
    and builds no Store. SIDEGRAPH_SUBAGENT_BRIEF=off disables it. Codex records trust per hook
    definition, so approve the new hook once in /hooks; manual recipes for both hosts gain the
    entry. See the hooks reference.

Changed

  • The capture nudge comes back after more work, instead of firing once per session. A session
    that runs for a day and spawns subagents used to get one chance to record what it decided, at
    its first substantial Stop, often before the work worth recording. A session that was already
    nudged is now nudged again when at least 30 minutes have passed since its last nudge and at
    least 10 commits authored since then are reachable from a local branch or HEAD; the text of
    that nudge ends with Since the last capture prompt: N commits., and the first nudge is
    unchanged. The count is one git log --branches HEAD, run in the project the store belongs to
    with a 2-second timeout, and it goes by author dates, so a rebase or cherry-pick of old commits
    is not new work. A git failure means no re-arm. The last nudge is the meta key
    capture_rearm:<session>, which the first nudge writes too and SessionStart expires after 30
    days; a session captured by an older version has no key and counts from the captured_at of its
    capture_sessions row. The new stamp is written with a compare-and-swap, so two parallel Stop
    hooks cannot both nudge. A Stop inside the 30 minutes still exits on the read-only peek and
    runs no git: a subprocess measured on a copy of this repository's store with a 1,000-commit
    repository takes about 39 ms inside the gap and about 47 ms past it with too few commits. The
    nudge's length bound is 850 characters, up from 800, for the added sentence. The count includes
    commits that other sessions or people make on the same machine's branches.
    SIDEGRAPH_CAPTURE_NUDGE=off silences re-armed nudges too. See the
    hooks reference.

  • PreToolUse hands the agent the records of a file when it reads or edits it, instead of
    asking it to fetch them.
    The hook used to print a one-line nudge (a count, or the titles of
    two records) and ask the agent to call get_task_context, which for a tool the host lists only
    by name means loading it first. In the field study the one nudge that named a record worked and
    the five that named counts did not, Edit and Write never nudged, and Bash, which is how current
    Claude Code reads and searches, was never wired. The hook now prints the records themselves: the
    guard line, then for each file a header and its top two records (three when the first two are
    mistakes), each as - [kind] title — first sentence of the choice (id …), accepted mistakes
    first, proposals last and marked [unratified], and a More: get_task_context(files=[…]) line
    when records were left out. It covers Read, Grep, Edit and Write, and four Bash read
    commands, sed, grep, rg and cat, wired as four if entries on one command (if needs
    Claude Code 2.1.89 for correct matching; an older host runs the hook on every Bash call, which
    exits early with no output). A Bash line delivers the first three files it reads that have
    records. Each file arrives once per agent, and an agent receives at most ten files: one SQL
    statement counts and inserts a pretool_file:<session>:<agent>:<path> key, where <agent> is
    - for the session's own agent, so concurrent hooks cannot exceed the cap. The counting form
    (this project has a decision memory … N domains, M decisions) is gone, the pretool_nudge:
    and pretool_nudge_path: keys are no longer written (SessionStart still prunes them, and the
    new keys after 30 days), and a Bash read is still not a touch event. SIDEGRAPH_GREP_NUDGE=off
    keeps its name and now turns off all of this
    , on every tool that reads (not the Agent
    brief below, which has its own switch); it stops neither the hook from
    launching on a Bash call nor touch recording. A subprocess median of nine on this repository's
    store: about 26 ms when nothing is delivered, about 35 ms for a Read of the file with the most
    records, about 38 ms for a Bash line naming three anchored files. A hand-wired
    .claude/settings.json gains the Bash group (see the
    setup guide). The PreToolUse
    commands, in the plugin and in that snippet, no longer set SIDEGRAPH_GRAPH, which the hook
    never read: Graphify's graphify claude install and uninstall remove a Bash entry whose
    text mentions graphify, and would otherwise have removed the hand-wired group. See the
    hooks reference.

  • The plugin's hot hooks start from the commit SessionStart resolved, not from @main.
    uv re-resolves a branch reference on every call, about 0.8 s on a warm cache, and Stop and
    PreToolUse run many times a session. SessionStart now records the commit uv resolved for
    an @main install in ${XDG_CACHE_HOME:-$HOME/.cache}/sidegraph/launch-commit: 40 hex bytes,
    one file per machine, never written by an install pinned to a tag or SHA, an editable install
    or a PyPI install. The public Stop and PreToolUse commands (Claude C...

Read more

v0.8.0

Choose a tag to compare

@SantyagoSeaman SantyagoSeaman released this 03 Oct 06:44

[0.8.0] — 2026-10-03

Added

  • get_task_context says where the memory is when your file has none. A file seed the graph
    lacks, or one that resolved with no current record anchored to it, now gets a ## Nearest anchored
    sentence naming up to three files that do carry records: the graph neighbours first (the two
    with the most records), then the files under the nearest directory that has any, skipping a
    directory that holds more than a quarter of all anchored files (and more than four) and never
    the repository root.
    When the answer has no decision memory at all, the records of those files follow under ## Nearest anchored records (not anchored to your files), labelled as not about your files, with
    the same supersede line the main answer carries. An empty answer carries the sentence under ## Why this is empty. At most three seeds get a sentence. The records count as shown in
    sidegraph-stats, and the neighbour files are never recorded as seeds. See
    the tool reference.

  • Records that never reach the team are now visible. A record follows the branch and
    checkout it was written on, and on one field corpus two supersessions sat uncommitted for five
    days and 32 proposals lived only on branches that main's session start never saw. Two new
    checks run at session start. store-uncommitted (degraded, with a notice, also in
    sidegraph-doctor and sidegraph-stats) reports store files nobody committed for a day, aged
    by the ULID in a new file's name (a git stash cannot rewrite it) and by the modification time
    otherwise; fully staged files do not count, so sidegraph-doctor --check in a pre-commit hook
    never blocks the fixing commit. branch-only-records (advisory) scans every unmerged local branch,
    oldest first, for open records the default branch lacks (each counted once, for the newest
    branch that holds it), and sends a notice when such a branch is more than a week old: the fix is
    to merge it. The write tools (add_decision,
    supersede_decision, add_fact, supersede_fact, add_anchors, add_domain,
    supersede_domain, and the written elements of propose_decisions and propose_domains) add a
    commit_hint to their result when the store is inside a git repository, except a store that is a
    symlink to a directory outside it: git commits the link, not the records behind it. Where records
    are written does not change. See the
    troubleshooting guide and the
    MCP tools reference.

  • Sidegraph tells you when it needs attention, not only the model. One list of integrity
    checks now runs at every session start and in sidegraph-doctor and sidegraph-stats. The model
    still gets a status line per problem, with the five existing lines unchanged and in the same
    order. Problems that need a person also reach you as a systemMessage, which Claude Code shows as
    a warning: at most once a day for each check, again at once when a problem gets worse, and not
    again once the check finds nothing. New checks: a store that cannot be opened (a merge conflict
    left in .sidegraph/, a damaged index, a store written by another version) now says so and
    names the fix instead of switching memory off silently; a missing or unreadable code graph;
    open records whose every code anchor is orphaned (degraded, with a notice, when three or more were
    written in the last 14 days; advisory otherwise); and store files the last reload could not
    index. A proposal that has waited 30 days earns a notice too. sidegraph-doctor gains an
    orphaned-record finding and, with no graph, a skipped-checks line ("graph" in --json's
    skipped) instead of a finding, so --check does not start failing CI jobs that run without
    one. sidegraph-stats opens with a HEALTH line (health in --json). The checks, their
    severities and fixes are in the new troubleshooting guide, and
    the output is in the hooks reference.

  • sidegraph-init asks whether to keep the code graph fresh, and installs the hook that does
    it.
    A graph nobody rebuilds goes stale, and on one field corpus it fell 314 commits behind
    because the setup hint was printed and skipped. In a terminal, init now asks once (default yes);
    --hooks installs without asking, --no-hooks declines and records it
    (git config sidegraph.graphRefresh false), and --remove-hooks takes everything out and exits
    without creating a store (it exits 1, and says the removal was partial, when a hook is a symlink
    or has damaged markers and so keeps its block). The hook is a marked block in post-commit, post-merge and
    post-checkout, inserted right after the shebang line so that a foreign hook that ends in exit
    cannot skip it, plus a helper that rebuilds the graph in the background, in the main checkout
    only (a linked worktree reads its graph), one rebuild at a time. It never replaces a hook: one that
    is a symlink, not executable, CRLF or not a shell script is left alone and the line to add by hand
    is printed. With core.hooksPath set it writes no hook file and prints the lines instead. Without
    a terminal nothing is written, and a new advisory check, refresh-hook-missing, tells the model
    to ask you, and tells you once a day, until the hook is installed or declined. A call you add by hand
    counts only when its hook is executable and the call is not in a comment. The three flags are
    a Committed surface (stability); see
    keeping the graph fresh and the
    troubleshooting guide.

Changed

  • get_task_context reads a wrong seed the way you meant it, and an empty answer says why.
    A seed the graph does not hold as written used to come back as "No context found." Now ./x.py,
    x.py:12, an absolute path inside the repository, a missing or invented directory prefix, a bare
    file name, a directory, and a Type.member with no file are read through a short ladder, and a
    ## How your seeds were read block says what each one became and that a rewrite is a guess. A
    file that exists on disk but not in the graph is never swapped for a namesake. A directory reads
    as up to eight files, or is listed when it holds more than 24. A path that differs from a graph
    file only by letter case reads as that file, even on a case-insensitive filesystem. A path or
    name that could mean several is reported and read as none. Guessed seeds rank in the related
    tier below the seeds you got right. The not-in-graph block now names the normalised path, so
    ./pkg/n.py gets the stale-or-new advice instead of "check the path". A name with no file that
    matches several symbols is no longer expanded to all of them: it is reported as ambiguous. A call
    with no seeds at all names the accepted domains you can drill into. Telemetry records a rewritten
    path under the path that was read and a directory under its normalised key, and keeps a seed
    whose name starts with .. that it used to drop. See
    the tool reference.

Fixed

  • A symlinked store's capture commit, code drift and refresh-hook check come from the project
    that holds the link.
    A store can be a symlink (.sidegraph -> ../shared/store). Its records
    are anchored to code in the project that holds the link, but git resolved the link and answered
    for the repository it points into: provenance.commit named that repository's HEAD, or was
    empty when the target sits in no repository, so the code-drift check passed silently or never
    ran, and the refresh-hook check read the wrong hooks. Capture now stamps the project's HEAD
    (empty while the project has no commits), the code-drift check diffs it in the project's
    repository, and the refresh-hook check reads the project's hooks and recorded choice, the ones
    sidegraph-init writes. A store that is not a symlink behaves as before, and
    --against (sidegraph-verify, sidegraph-doctor) still diffs the history of the repository
    that holds the store's files. Three behaviour changes come with it. A store directory that is itself a repository or
    submodule inside another repository now stamps the enclosing project's HEAD and measures drift
    there, and its refresh-hook check now runs against the enclosing project; a top-level store
    repository with no enclosing one is unchanged. A record stamped before the fix with the link
    target's (or the nested repository's) HEAD now gets sidegraph-doctor's existing
    code-drift: git unavailable — check skipped note for that commit, where it used to pass
    silently. And the records of a symlinked store now match in git-bindings rule (a) and in
    sidegraph-blame, which read provenance.commit. See the store
    format
    .

Security

  • The lockfile moves PyJWT from 2.13.0 to 2.15.1 and virtualenv from 21.7.0 to
    21.14.4.
    The PyJWT 2.14.0 and 2.15.0 fixes cover one critical advisory (an asymmetric
    PEM key with mutated whitespace skips the HMAC key-confusion guard), five high ones (key
    material accepted as HMAC secrets, JWKS redirects, a BOM bypass) and several moderate
    ones. The virtualenv fixes cover seed wheels that were not integrity-checked and
    activation scripts that run commands embedded in a path or a prompt. PyJWT reaches Sidegraph only
    through the MCP SDK's crypto extra, and Sidegraph never decodes a token; virtualenv is
    a development dependency through pre-commit. The published package pins neither, so this
    changes development and CI environments and installs made from uv.lock; a fresh install
    alr...
Read more

v0.7.0

Choose a tag to compare

@SantyagoSeaman SantyagoSeaman released this 02 Oct 05:00

[0.7.0] — 2026-10-02

Changed

  • The hooks and the MCP server look a missing relative store up inside the repository. The
    precedence between --db, SIDEGRAPH_DIR, SIDEGRAPH_DB and the default is unchanged, and a
    store that exists where its relative path is anchored still wins. What is new: when a relative
    SIDEGRAPH_DIR (or the default .sidegraph) does not exist there, the three hooks and the MCP
    server visit the parent directories, nearest first, up to and including the repository root (the
    nearest directory holding a .git entry, a directory or a file), and use the first store found.
    The lookup never leaves the repository (a .git entry at your home directory or above it is not
    a repository root, so a dotfiles repository does not turn ~/.sidegraph into every project's
    store) and does not run for --db, an absolute SIDEGRAPH_DIR, SIDEGRAPH_DB, a value with a
    .. part, or an anchor that is a symlink. The CLI, sidegraph-init
    and sidegraph-bootstrap are unchanged: from a subdirectory they still need --db. To keep the
    old outcome on a host surface, pass an absolute SIDEGRAPH_DIR.

Fixed

  • A SessionStart hook registered twice on one host no longer injects the map twice. Two
    copies firing for one session at once both read the old duplicate-check ledger and both emitted;
    the check now decides and stamps inside one write transaction, and a duplicate still answers
    without waiting for a lock. The ledger keeps one row, so a repeat is still let through when
    another session starts in between, or when two sessions with doubled hooks start together.

  • Sync's moved rung adopts only a real move. When an anchor's file is gone, sync looks the
    name up across the whole graph and follows a unique hit to its new file. That lookup ignores
    case and decoration, and the check behind it (the old path is out of HEAD, the new one is in
    it) did not ask whether the new path was there before. A type deleted together with its file
    could therefore be re-anchored onto an unrelated file that held an old member of the same name:
    a deleted Priority followed .priority into another file, silently rewriting the entity's
    committed descriptor.file_path. Two rules now close it. The hit must be the same name once
    the call decoration ((), a leading .) is stripped from both sides, with case kept. And git
    must show the new path arriving in the change that removed the old one: the mainline commit that
    deleted the old path (a branch merged with --no-ff counts as one change) must have a first
    parent without the new path and must hold it itself. With SIDEGRAPH_TRUST_DIRTY_TREE=on and the
    old path still in HEAD, the new path must be absent from HEAD. Every git failure refuses, as
    does a history with no deletion in it, so a move out of a path git never held (a gitignored
    directory) and a move that a shallow clone cannot prove now end orphaned, which
    sidegraph:heal-anchors repairs, instead of being adopted. An uncommitted delete onto a path
    that is already in HEAD is orphaned too, not remembered as a move waiting for a commit: no
    commit can make it one. The search matches the old path literally (a path such as
    app/[id]/view.tsx is not a glob) and ignores log.showSignature.

    Some real moves are refused as well, because git's history alone does not show them as one
    change. Each ends orphaned and sidegraph:heal-anchors re-anchors it:

    • the new file added in one commit and the old one deleted in another on the first-parent line,
      including a branch merged fast-forward or rebase-merged;
    • a second move before any sync ran (A to B to C while the descriptor still names A);
    • an old path that was re-added and then deleted again.

    To check what an earlier release adopted, list the descriptor rewrites:
    git log -p --diff-filter=M -- .sidegraph/entities/ | grep -E '^(commit |[-+]\s+"file_path")'.
    A rewrite is a real move only if the new path was added by the commit that deleted the old one
    (git log --first-parent -m -1 --diff-filter=D -- <old path> finds that commit; the new path
    must be missing from its first parent and present in it). Re-anchor a wrong one by superseding
    the record with fresh anchors.

  • The SessionStart instruction names a call the tool accepts, and a domain's mistake count
    counts what the domain holds.
    The standing line told the agent to call
    get_task_context(seeds), but the tool takes files and entities and rejects anything else,
    so an agent that followed it made a call that failed validation. It now says
    get_task_context(files=[…]) with repo-relative paths, points at the check-plan skill before a
    non-trivial change when it is available, and says to load the tool first if the host lists it
    only by name. Separately, the per-domain count in the SessionStart map read "0 mistake(s)"
    over a domain whose gotchas were anchored to its own code, because it counted only decisions
    tagged to the domain itself. It now counts the accepted gotcha, lesson and constraint
    decisions that drill_down serves for the domain, so the two show one number. The map's cache
    records the store's digest, so a retrieval call on an unchanged store no longer rebuilds it, and
    SessionStart reads the unratified list without building the whole map. The bindings table gains
    an index by entity (an existing store gets it on its next open), which takes a rebuild of the map
    from about 0.1 s to under 0.03 s on a real store, so the cost of a session start is what it was
    before the count changed, also right after a store change and in a linked worktree. A worktree
    builds its map with the main checkout's graph reader, so it counts the same decisions the main
    checkout does. A map written by a path that has no graph reader (ratify, capture) leaves out
    decisions anchored to a whole document until the next sync.

  • A Claude Code subagent gets its own read nudges, and its touches are told apart. A
    subagent's hook payload carries its parent's session id plus an agent_id of its own, and
    the PreToolUse nudge keys were per session: once the main agent had been nudged, every
    subagent it started read anchored files blind. Each agent, the session's own and every
    subagent, now gets one generic and one path-specific nudge. The
    keys are claimed in a single statement, so a subagent's parallel opening reads cannot both
    nudge, and SessionStart expires keys older than 30 days. The retrieval_events journal
    gains a nullable agent column, added to an existing index on its next open, that records
    which subagent made a touch; the row stays under the parent's session, so sidegraph-stats
    counts the same sessions as before. Only agent_id counts, never agent_type: a main
    session started with claude --agent <name> is still the session's own agent.

  • An anchor written Type.member now resolves to the member. Graphify labels a method
    .playClip() and links it to its type, an agent writes AudioPlayback.playClip, and the two
    never met: the anchor was born orphaned, and a seed naming it never reached the record, which
    surfaced only through its community, as an unratified proposal. With a file_path, a name
    with no exact match now falls back to a member of the named type in that file. It is for code
    identifiers only (prose, a numbered heading or a hyphenated file name never reads as a
    member). The name is split at the last ., :: or # and the member is compared with its
    case kept. A candidate that an edge ties to a different type is refused, and the answer stays
    orphaned, never guessed, when several remain or when the file holds a case-only twin of the
    member (a struct Message beside a method .message(), which the store treats as one
    anchor). Without a file, and for a path-qualified name (src/pkg/mod.py), nothing changes.
    On the read path a seed that names a stored anchor, and every node a seed resolves to, now
    map to their store entities by the engine mapping, in the neighbour walk as well, so such a
    record ranks as a mistake or decision again. The first sidegraph-sync after the upgrade
    reruns once on its own (the sync stamp now carries a resolver revision, which a report does
    not show) and heals the orphans this left behind. The skills and guides no longer say
    Type.member names never resolve; the bare name with its file stays the preferred form.

  • A hook command that cannot start no longer blocks or loops the host. On Stop, both hosts
    feed a hook's stderr back when the hook exits 2: Codex continues the turn, and Claude Code
    continues the conversation, and Claude Code also blocks the tool call on PreToolUse. uv
    exits 2 on some of its own errors (a project it cannot find, a cache it cannot write) and dash
    exits 2 when the cd fails, so a hook command that never reached Python could do exactly that,
    over and over in a headless codex exec run. Every shipped hook command, in both plugins and in
    the manual recipes in the docs, now ends with a guard: Stop and PreToolUse answer {}, and
    SessionStart answers a one-line systemMessage that says the hook could not start and to run
    the hook command in a terminal to see the error. A command that starts Python behaves as
    before. Codex users: approve the two hooks again once after updating. Codex records trust
    per hook definition and the definition includes its command, so the changed commands do not run
    until you approve them in /hooks or at the next interactive session's trust prompt. If you
    wired the hooks by hand
    , append the guard to your own commands in .claude/settings.json or
    .codex/hooks.json; the recipes in the docs show it. The Codex page also now says how to run a
    headless review without Sidegraph's hooks.

  • The capture nudge now arms on Codex. The Stop hook's substance gate ...

Read more