Glyphtrail

unsafe forbidden

Local code intelligence graph for AI coding agents: Tree-sitter static analysis, structural code search, dependency impact, blast-radius analysis, CLI and MCP server.

Glyphtrail is local-first code intelligence: it builds a code knowledge graph from your repositories so AI coding agents (and you) can query structure, trace lineage, and discover recurring ideas across time. Tree-sitter based static analysis extracts symbols, calls, imports, inheritance, design-rationale comments, and cross-boundary API links into a per-repo LadybugDB graph.

Over that graph it powers structural code search, call graph and dependency graph traversal, and blast-radius / impact analysis: a GraphRAG substrate for codebases that agents reach through an MCP server for code intelligence or the CLI. Use it as a repo index for Claude Code, Cursor, Windsurf, and Codex.

Built native in Rust.

Install

Prebuilt binaries for Linux, macOS (Intel + Apple Silicon) and Windows are attached to each tagged release.

Or build from source:

cargo install --git https://github.com/glyphtrail/glyphtrail glyphtrail-cli
# or, from a checkout:
cargo build --release   # binary at target/release/glyphtrail

Usage

# Index the current repository (writes .glyphtrail/ladybug)
glyphtrail analyze .

# Re-index only files that changed
glyphtrail analyze . --update

# Index a whole directory of repositories: discover every repo under the path
# (descending into nested repos), analyze each, and register them all
glyphtrail analyze ~/code --recursive          # add --hidden to scan dot-dirs

# Index a remote repository: clone into ~/.glyphtrail/remote/<host/owner/repo>,
# index it there, and register it (shallow by default; --full for history)
glyphtrail analyze https://github.com/owner/repo
glyphtrail analyze [email protected]:owner/repo.git --full
glyphtrail remote list                 # cloned remotes + their local paths
glyphtrail remote remove <name>        # delete a clone (files + index) + unregister
#   (glyphtrail remote remove --all  clears them all)

# Outline the shape of a file or directory (symbols + signatures)
glyphtrail outline src/api.rs              # standard detail
glyphtrail outline src --detail minimal    # names only, whole directory

# Query the graph
glyphtrail query def <name>            # locate a definition
glyphtrail query callers <name>        # who calls it
glyphtrail query callees <name>        # what it calls
glyphtrail query neighbors <name>      # direct graph neighbours
glyphtrail query search <text>         # full-text search (names + doc comments)
glyphtrail query impact <name>         # transitive blast radius if it changes
glyphtrail query history <name>        # commits that touched the symbol across
#   all branches, each flagged in HEAD or not — finds a fix parked on an
#   unmerged branch ("where did that fix go?"); add --prs to map it to an open PR
#   add --json (or --yaml, compact for agents) for machine-readable output

# Impact analysis (blast radius from a symbol, file, or change set)
glyphtrail impact <name>                       # seed: a symbol
glyphtrail impact --file src/api.rs            # seed: every symbol in a file
glyphtrail impact --since main..HEAD           # seed: changed symbols vs a git range
glyphtrail impact --staged | --diff            # seed: staged / working-tree changes
#   reports a risk level (LOW/MEDIUM/HIGH/CRITICAL) and, for a large blast radius,
#   a capped summary — add --details (full list) or --limit N
#   [--cross-boundary] reach API consumers (HANDLES/INVOKES/EXPOSES/MOUNTS)
#   [--edges calls,refs,imports,impl,api] [--depth N] [--min-confidence extracted|inferred]
#   [--format text|json|md]   [--gate]  exit 2 when the change touches the API surface

# Visualize
glyphtrail viz --output graph.html     # self-contained interactive page
glyphtrail serve --port 7700           # live explorer at http://127.0.0.1:7700

# Agent integration (Model Context Protocol)
glyphtrail mcp                         # MCP server over stdio (query/endpoints/impact/…)
#   `glyphtrail serve` also exposes the same tools at POST /mcp (JSON-RPC)
glyphtrail setup --local               # onboard agents in THIS repo: write
#   .claude/skills + a managed CLAUDE.md/AGENTS.md section pointing them at the
#   MCP/CLI, and gitignore the index. Idempotent and stats-free.
glyphtrail setup --user                # same, but user-wide in your home dir
#   (pick --local and/or --user explicitly — a repo-local install lands in commits)
glyphtrail setup --local --gitignore   # keep it local-only: write the skill but
#   gitignore it, skip the CLAUDE.md/AGENTS.md patch, and strip a section a prior
#   run added (so nothing glyphtrail-related is committed).

# Generate a docs wiki from the graph via an LLM
glyphtrail wiki --provider claude        # or openai / openrouter (reads *_API_KEY)
glyphtrail wiki --dry-run                # write the prompts only (no network/keys)
#   --base-url lets an OpenAI-compatible gateway (e.g. Kilo) stand in

# Stats (also reports index freshness: fresh / stale / unknown)
glyphtrail status
#   read commands (query, impact, outline, drift, cypher) print a stderr note
#   when the index looks stale (HEAD moved, uncommitted changes, or analyzer
#   upgraded) so you know to re-run `glyphtrail analyze`

# Inspect/edit the per-repo config (glyphtrail.toml) without hand-editing
glyphtrail config show
glyphtrail config set security.record_sensitive_files true   # validated before write
glyphtrail config get impact.test_globs
glyphtrail config unset impact.test_globs

# Atlas — an opt-in, private, local-only cross-repo history + embedding index
glyphtrail atlas init                       # create the store (~/.glyphtrail/atlas)
glyphtrail atlas sync [--everyone]          # ingest registered repos' git history
glyphtrail atlas status | timeline | story  # state · chronological log · LLM narrative
glyphtrail atlas embed repos [--provider openai]  # embed each repo from its digest
#   (local lexical model by default — no network; openai/Ollama opt-in, announced)
#   also: embed graph | commits | export | import | restore
glyphtrail atlas similar repos <repo|"free text">  # rank repos (or `similar commits`)
glyphtrail atlas digest [repo] [--json]     # structured repo digest: languages/deps/API
glyphtrail atlas viz -o atlas-map.html      # repo-similarity map (force-graph HTML)
glyphtrail atlas serve --port 8351          # serve the similarity map
glyphtrail atlas waka sync                  # pull WakaTime time-tracking (off-machine)
glyphtrail atlas waka show                  # effort per repo · language/editor/device

Keys for the OpenAI embedder / WakaTime are read from the environment; a .env file in the working directory (or an ancestor) is loaded automatically.

atlas sync keeps only your commits by default (pass --everyone for all authors). Tell it who you are in the [me] section of ~/.glyphtrail/atlas/atlas.toml (a commented template is written on atlas init):

[me]
emails   = ["[email protected]", "[email protected]"]      # exact addresses
domains  = ["example.com"]                           # any address @ a domain you own
patterns = ["you+*@gmail.com", "*@*.example.com"]    # globs (* / ?) over the whole address

An address is yours if it matches any emails entry, sits at an owned domain, or matches a patterns glob — all case-insensitive.

Classify whole organizations or naming conventions as work with [repos] globs (matched against the repo name and each forge id host/owner/repo):

[repos]
proprietary = ["*acme*", "*/acme-corp/*"]   # hidden from public/default atlas views
private     = ["*-internal"]
public      = ["*/acme-corp/oss-*"]          # allowlist — wins, keeps these public

public is an escape hatch (it wins over the others) for a public repo caught by a broad work-org glob. The other patterns only raise restrictiveness; restricted repos are gated out of story, export, and the default similar/viz output (pass --include-restricted to include them). CTRL-C during a sync stops gracefully at a safe point (the database stays consistent); a second CTRL-C aborts.

Registering glyphtrail as an MCP server

glyphtrail mcp speaks the Model Context Protocol over stdio, exposing the query / impact / outline / endpoint tools to an agent. Index the repo first (glyphtrail analyze), then register the server. Each agent wires up MCP servers differently:

Claude Code (CLI) — add it to the system:

claude mcp add -s user glyphtrail -- glyphtrail mcp

or, if you prefer, to a specific repo only:

claude mcp add glyphtrail -- glyphtrail mcp --repo .

Claude Desktop — download the glyphtrail-<version>-<target>.mcpb bundle for your platform from a release and open it (Settings → Extensions, or double-click). The bundle ships the glyphtrail binary; nothing else to install. Because a Desktop extension is global, it starts without a pinned repository: the agent names the target repo (a registered name or path) on each tool call, and the list_repos tool discovers what is indexed. To wire it up by hand instead, add to claude_desktop_config.json (pass --repo to pin one repo, or omit it for the same repo-per-call behavior as the bundle):

{
  "mcpServers": {
    "glyphtrail": {
      "command": "glyphtrail",
      "args": ["mcp", "--repo", "/path/to/your/repo"]
    }
  }
}

Cursor / Windsurf / VS Code / Codex — point the client at the same stdio command. For example, a project-scoped Cursor .cursor/mcp.json:

{
  "mcpServers": {
    "glyphtrail": {
      "command": "glyphtrail",
      "args": ["mcp", "--repo", "."]
    }
  }
}

OpenCode / Kilo Code — these use a different shape and location: a single command array (program + args together) under a type: "local" server, in ~/.config/opencode/opencode.json or ~/.config/kilo/kilo.jsonc respectively:

{
  "mcp": {
    "glyphtrail": {
      "type": "local",
      "enabled": true,
      "command": ["glyphtrail", "mcp", "--repo", "."]
    }
  }
}

The exact key, format, and config location vary by client (some split command/args, some take one array; some are project-scoped, some global) — but the server is always the same: run glyphtrail mcp --repo <path> over stdio. Omit --repo (as the Desktop bundle does) and the server pins no repository: every tool call must then name a repo, and list_repos lists the indexed ones. (glyphtrail serve additionally exposes the same tools over HTTP at POST /mcp, for clients that prefer a URL.)

Prefer it managed? glyphtrail setup --local (or --user for a home-wide install) writes an agent skill plus a CLAUDE.md/AGENTS.md section that points coding agents at these tools.

Configuration

All per-repo settings live in one file: glyphtrail.toml at the repo root (committed, team-shared), with an optional .glyphtrail/glyphtrail.toml personal override (gitignored) merged on top — analysis settings override, [[links]] union. Every field has a default, so the file is only needed to change one. It holds:

  • [[api.schemas]] — blessed API schemas (OpenAPI / GraphQL SDL / protobuf / Hasura) to link code endpoints against and reconcile with glyphtrail drift.
  • [[languages]] — extra tree-sitter grammars to load at runtime.
  • [impact] test_globs — globs that mark a file as a test in impact reports.
  • [security] record_sensitive_files — record sensitive files as content-less nodes (see below) instead of skipping them.
  • [[links]] — manual cross-repo link hints (see Cross-repo blast radius), edited with glyphtrail repo link.

Edit it without hand-writing TOML; every change is validated against the schema before it is written, so a wrong key or type is rejected and the file untouched:

glyphtrail config show                       # current settings (or "defaults in effect")
glyphtrail config set security.record_sensitive_files true
glyphtrail config set impact.test_globs '["**/*_test.rs","tests/**"]'
glyphtrail config get impact.test_globs
glyphtrail config unset impact.test_globs
glyphtrail config set --local security.record_sensitive_files true   # personal override

Values are parsed as TOML, so true, 42, and ["a","b"] keep their types. (Old split files — .glyphtrail/config.toml, glyphtrail.links.toml, .glyphtrail/links.toml — are still read, and folded into glyphtrail.toml on the next config/link edit.)

Excluding sensitive files

analyze honors .gitignore/.git/info/exclude, skips dotfiles, and reads exclusion lists from .glyphtrailignore, .aiignore, .aiexclude, and .claudeignore. List any file with secrets/key material there to keep it out of the index entirely — and therefore out of every agent-facing surface (wiki, MCP).

A user-wide ignore file at ~/.glyphtrailignore applies to every repo you analyze — handy when bulk-indexing a whole work directory. It does two things:

  • gitignore-format patterns (e.g. *.generated.rs) exclude matching files in every repo's walk; a repo's own ignore files can re-include with !.
  • absolute (or ~/…) path lines exclude that whole repo/tree — analyzing it, or any subfolder of it, does nothing. Useful to skip a few giant repos while bulk-scanning a work directory.

Impact reports in CI

Seed the impact analysis from a pull request's diff and post a Markdown summary, optionally failing the job when the change touches the public API / schema surface (drift gate):

# .github/workflows/impact.yml
name: impact
on: pull_request
jobs:
  impact:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0           # need the base ref for the diff
      - run: cargo install --path crates/glyphtrail-cli   # or download a release binary
      - run: glyphtrail analyze .
      - name: Impact report
        run: |
          glyphtrail impact --since "origin/${{ github.base_ref }}...HEAD" \
            --cross-boundary --format md >> "$GITHUB_STEP_SUMMARY"
      # Optional: fail the job if the PR changes the API/contract surface.
      - name: Drift gate
        run: |
          glyphtrail impact --since "origin/${{ github.base_ref }}...HEAD" \
            --cross-boundary --gate

Cross-repo blast radius

Register repositories once, then trace a change's impact across them — when you change a crate that other locally-indexed repos depend on (via crates.io, git, or a path dependency), impact --downstream reports which of those repos break and where, not just your own.

glyphtrail repo add .                  # register the current repo
glyphtrail repo scan ~/code            # register already-indexed repos under a tree
glyphtrail repo scan ~/code --analyze  # index each repo found, then register it
glyphtrail repo scan ~/code --all      # register every repo, indexed or not
glyphtrail repo list                   # registered repos + health + forge ids
glyphtrail repo list --mine            # only repos you've contributed to
glyphtrail group add svc api core      # optional: a named subset to scope to

glyphtrail analyze .                   # index each repo as usual

# Blast radius extended into downstream repos that depend on this one:
glyphtrail impact MySymbol --downstream            # federate over the registry
glyphtrail impact --since main..HEAD --group svc   # scope to a group
glyphtrail impact MySymbol --downstream --deep     # re-read identities from each
#   member store (not the cached shortcut) and include indexed repos beside this
#   one that were never `repo add`ed — for when the cache may be stale/incomplete

Cross-repo links are matched by package name (Cargo, npm, Go, Python, .NET): a consumer's dependency is tied to the producer repo whose package publishes it. The MCP impact tool takes the same downstream/group arguments, and list_repos enumerates the registry, so an agent gets the cross-repo blast radius in one call.

Manual link hints. Some real relationships can't be matched by package name — a service called over HTTP from another repo with no shared package. Declare those as [[links]] in glyphtrail.toml (committed) and/or .glyphtrail/glyphtrail.toml (personal); both are unioned and fed into the federated impact alongside the auto-resolved links. from is the consumer, to the producer (changing to impacts from); each side's repo defaults to . (this repo), so you only name the other one. Omit a symbol for a coarse whole-repo link. Edit them with glyphtrail repo link add/list/remove (a thin wrapper over the same file).

# in web-client's glyphtrail.toml: we call user-svc's endpoint (no shared package)
[[links]]
from = { symbol = "fetchUser" }            # repo "." = here
to   = { repo = "user-svc", symbol = "get_user" }

Edit hints without hand-writing TOML with glyphtrail repo link:

glyphtrail repo link add user-svc --to-symbol get_user --from-symbol fetchUser
glyphtrail repo link add user-svc                 # coarse whole-repo link
glyphtrail repo link add user-svc --local         # write the gitignored personal override
glyphtrail repo link list                         # show hints (shared + local) with indices
glyphtrail repo link remove 1                      # drop a hint by index

How it works (many local databases, one registry)

There is no central database. Each repo owns its index at <repo>/.glyphtrail/ladybug, built independently by analyze. The only shared state is the user-wide registry at ~/.glyphtrail/registry.json, which just maps repo names to their roots (plus forge ids and groups) — it holds no graph data.

A federated query (impact --downstream / --group, or the MCP impact tool) stitches those independent indexes together at query time:

  1. Select the members — the whole registry, or a named group. The current repo (matched to a registry entry by path) is always included.
  2. Open each member's index read-only and pull its persisted package identity: the packages it publishes (with their public exports) and its external uses (imports that reference a declared dependency). Both were computed and stored during that repo's own analyze.
  3. Resolve cross-repo links by matching a consumer's external use to the producer repo whose package name (and exported symbol) it references. This yields edges from a producer's export to the exact consumer symbols that use it (falling back to file-level, then crate-level, when a symbol can't be pinned down).
  4. Traverse the normal impact engine over the union, where those cross-repo edges connect the per-repo graphs, and report the blast radius grouped by repo (origin first, then downstream).

Because linking is derived from package metadata, a repo only participates once it has been analyzed (so its identity is on disk) and registered; a repo with no recognized package/dependency relationship to the others simply contributes no cross edges. Linking is Cargo-only today.

repo scan walks for version-control roots (.git/.svn/.bzr/.hg), skipping dot-directories (--hidden to include them) and treating each repo as a boundary (--recursive to also find nested repos / submodules). A submodule's code is never indexed as part of its parent — analysis stops at nested-repo boundaries — so it lands only in its own index. The walk and per-repo work show progress, which matters on slow network-backed drives.

Repository identity

Each registered repo gets stable forge identities derived from its git remotes, so it is recognised across folder renames, multiple clones, and name collisions with published crates — and a repo's mirrors (e.g. a GitHub origin plus a Codeberg mirror) all resolve to the same repo. Two kinds, recorded at repo add:

  • Slug (always, offline): host/owner/repo from each remote → a UUIDv5.

  • Numeric (optional, rename-proof): the forge's numeric repo id via its API, which survives a rename on the forge. Resolved only when a token is available, per forge:

    Forge Host Token env var
    GitHub github.com GITHUB_TOKEN (else falls back to the gh CLI)
    GitLab gitlab.com GITLAB_TOKEN
    Gitea / Forgejo codeberg.org CODEBERG_TOKEN

Numeric ids are entirely opt-in: with no token (and no gh), only the slug ids are recorded. Tokens are read from the environment and never logged.

glyphtrail repo refresh [name] re-derives ids for registered repos from their current git remotes, in place — handy when remotes change or to repair ids written by an older version, without re-analyzing.

The same repo can live at more than one path — a symlink, a second clone, a backup copy. Glyphtrail keeps one entry per repo: when a path you register shares a forge id with an existing entry, it's folded in as an additional location (shown as also at … in repo list) rather than a duplicate, so the repo isn't double-counted in federated impact. Symlinks collapse automatically via path canonicalization.

Each repo records its top git authors at registration, so repo list --mine (or --author <name-or-email>) narrows a large registry to the repos you've worked on without scanning each repo's history.

For tokens under a non-standard env var, or self-hosted Gitea/GitLab/Forgejo instances the tool can't recognise by host, map them in ~/.glyphtrail/forge.toml:

[hosts."codeberg.org"]
token_env = "CODEBERG_READ_ONLY_PAT"   # use this var instead of CODEBERG_TOKEN

[hosts."git.example.com"]
kind = "gitea"                          # github | gitlab | gitea
token_env = "EXAMPLE_TOKEN"

Concurrent writes and recovery

The registry (~/.glyphtrail/registry.json) and groups files are updated under a portable lock file (registry.lock) that works on network / FUSE / sync filesystems (e.g. pCloud), where OS advisory locks (flock) are unreliable and can leave a permanent lock when a writer dies. The lock self-heals: a lock held by a dead process on this host, or one older than a couple of minutes, is stolen automatically on the next run, so a crashed repo add never wedges the registry.

If the lock is genuinely busy (another repo add is mid-write), repo add doesn't fail or block — it writes the entry to a spillover file (registry.spill.*.json) beside the registry, and the next run that holds the lock merges it in. Spillovers are written atomically (temp + rename), so a reader never sees a half-written file. This makes bulk registration (indexing many repos at once) loss-proof even on a slow or contended drive.

As a manual escape hatch, glyphtrail repo unlock force-releases the lock. It is rarely needed given the self-healing above, and only removes the lock file.

Languages

Coverage is driven by a tree-sitter grammar registry. Built in: Rust, Python, JavaScript, TypeScript/TSX, Go, Java, C, C++, C#, Ruby, Kotlin, Bash, PHP, Scala, OCaml, Haskell, Lua, Swift, Elixir, Zig, R, Dart. Adding a built-in language is a grammar in glyphtrail-parse/src/registry.rs plus a query file under glyphtrail-parse/queries/.

Extra languages can also be loaded at runtime without rebuilding — point glyphtrail.toml at a tree-sitter grammar and a query (the grammar is compiled on demand; needs a C toolchain):

[[languages]]
name = "ruby"
extensions = ["rb"]
grammar = "grammars/tree-sitter-ruby/src"   # dir with parser.c + grammar.json
query = "queries/ruby.scm"                   # @def.<kind>/@call/@import/…

Graph model

  • Nodes: repo, file, module, function, method, class/struct/interface/enum/ trait, and design-rationale comments (NOTE/WHY/HACK/TODO/FIXME).
  • Edges: contains, calls, imports, extends, implements, documents. Each edge is tagged extracted (straight from the AST) or inferred (resolved heuristically across files).

Architecture

A Cargo workspace:

Crate Responsibility
glyphtrail-core domain model, language detection, config
glyphtrail-parse tree-sitter registry, extraction, graph building
glyphtrail-store LadybugDB (Cypher) storage and graph queries
glyphtrail-viz Cytoscape graph rendering (HTML/JSON)
glyphtrail-server axum server for the interactive explorer
glyphtrail-mcp Model Context Protocol server (stdio) exposing the query tools
glyphtrail-cli the glyphtrail binary

Storage sits behind the GraphStore trait. LadybugDB (Cypher, native graph traversal) is the storage backend; the .glyphtrail/ladybug index is the source of truth.

glyphtrail analyze .                    # writes .glyphtrail/ladybug
glyphtrail cypher "MATCH (n:Node) RETURN n.name LIMIT 10"   # raw Cypher

LadybugDB links the lbug crate, which downloads a prebuilt liblbug or builds from source via cmake + a C/C++ toolchain (clang/gcc) — install those if the prebuilt archive is unavailable (e.g. apt-get install cmake clang build-essential).

Development

Prerequisites

  • Rust toolchain: install from rustup.rs
  • task - task runner (Taskfile.dist.yaml)
  • prek - fast Rust-native pre-commit hook runner (cargo install prek)

Setup

# Clone and build
git clone https://github.com/glyphtrail/glyphtrail
cd glyphtrail
cargo build --workspace

# Install pre-commit hooks (runs on every `git commit`)
prek install

Checks

task fmt        # format in place
task lint       # clippy -D warnings
task test       # full test suite
task ci         # fmt:check + lint + test (mirrors CI)

# Run hooks manually against all files
prek run --all-files

The CI matrix runs against stable and the MSRV (1.95).

License

European Union Public Licence 1.2