No description
  • Python 82.8%
  • JavaScript 14.7%
  • CSS 1.5%
  • HTML 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-16 12:53:34 -04:00
.claude/skills/obsidian-to-corkboard Add custom packs, common attributes, and cleaner Obsidian conversions 2026-09-16 12:53:34 -04:00
docs/prompts Add custom packs, common attributes, and cleaner Obsidian conversions 2026-09-16 12:53:34 -04:00
src/corkboard Add custom packs, common attributes, and cleaner Obsidian conversions 2026-09-16 12:53:34 -04:00
tests Add custom packs, common attributes, and cleaner Obsidian conversions 2026-09-16 12:53:34 -04:00
.gitignore Add custom packs, common attributes, and cleaner Obsidian conversions 2026-09-16 12:53:34 -04:00
.python-version Phase 1: core models, storage, packs, CLI 2026-09-16 08:16:36 -04:00
CLAUDE.md Add custom packs, common attributes, and cleaner Obsidian conversions 2026-09-16 12:53:34 -04:00
pyproject.toml Add README with installation, usage, and feature guide 2026-09-16 11:05:12 -04:00
README.md Add custom packs, common attributes, and cleaner Obsidian conversions 2026-09-16 12:53:34 -04:00
uv.lock Phase 2a: read-only web board 2026-09-16 08:39:42 -04:00

corkboard

A local-first graph database for mapping people, places, organizations, and the connections between them, then exploring that web visually. Think of the red-string conspiracy wall, but queryable, sourced, and private.

corkboard serves two kinds of work with one engine:

  • Tabletop RPG campaigns: NPCs, factions, locations, items, plot hooks, and who knows what about whom, with GM secrets kept apart from what the players have learned.
  • OSINT investigations: people, organizations, accounts, domains, emails, phone numbers, addresses, and events, with every claim tied to the sources that back it.

Everything stays on your machine. There is no telemetry, no cloud sync, and no network access unless you explicitly enable a plugin that needs it.

Contents

Core ideas

corkboard stores five kinds of records:

Record What it is Example
Entity A thing on the board, with a type and a label npc "Mira Vell", domain "example.com"
Claim One attribute of an entity, stored as its own record alias = The Heron, dob = 1984
Relationship A typed link between two entities Mira Vell member_of The Gilded Heron
Source Where information came from, rated for reliability "Session 3", "Company registry extract"
Evidence Links a source to a claim or relationship, rated for credibility Session 3 supports the alias, credibility 2

A few principles shape how these behave:

  • Every fact can carry provenance. Claims and relationships get their ratings from the evidence behind them.
  • Nothing is silently overwritten. An entity can hold several claims for the same key (two addresses, two conflicting birth years), each with its own sources.
  • Facts have time. Claims and relationships can say when they were true, including approximate dates ("circa 2019") and fictional calendar dates ("3 Harvestmoon 1042 AR").
  • Visibility is built in. Any record can be tagged (for example gm, players, redacted), and every view and export respects those tags.
  • Workspaces never mix. A campaign and an investigation live in separate files.
  • Automation suggests; you decide. corkboard suggests merges, credibility upgrades, and plugin findings, but never applies them on its own.

Installation

Requirements: Python 3.12 or newer and uv.

git clone <this repository> corkboard
cd corkboard
uv venv --python 3.12
uv sync
uv run corkboard --help

All commands below are run with uv run corkboard ... from the project directory. If you install the package into another environment, the corkboard command works the same way.

Quick start

Create a campaign, add a few records, and open the board:

# Create a workspace (a directory) for an RPG campaign using the rpg-core schema pack.
uv run corkboard init ~/campaigns/ashmoor --kind rpg --packs rpg-core

# Tell corkboard which workspace to use, so you can skip -w on every command.
export CORKBOARD_WORKSPACE=~/campaigns/ashmoor

# Add entities. Each add command prints the new record's id.
MIRA=$(uv run corkboard entity add npc "Mira Vell" --tags smuggler)
GUILD=$(uv run corkboard entity add faction "The Gilded Heron")

# Add a source, then a claim backed by it (rated B2), visible to the players.
SESSION=$(uv run corkboard source add session "Session 3" --reliability B)
uv run corkboard claim add $MIRA alias "The Heron" -v players --source $SESSION -c 2 --excerpt "tavern scene"

# Link the two entities.
uv run corkboard rel add $MIRA $GUILD member_of -v players

# Look at Mira as the GM, then as the players see her.
uv run corkboard entity show $MIRA
uv run corkboard entity show $MIRA --as players

# Open the visual board in your browser.
uv run corkboard serve ~/campaigns/ashmoor --open

For an investigation, use --kind osint --packs osint-core instead.

Workspaces

A workspace is a directory holding one SQLite database and an archive folder for captured files:

ashmoor/
  workspace.db
  archive/
uv run corkboard init PATH --kind rpg|osint [--packs rpg-core,osint-core] [--mode warn|strict]
  • --kind rpg also creates a built-in gm-canon source (reliability A) for ground truth the GM enters directly.
  • --packs chooses which schema packs define the entity and relationship types.
  • --mode sets validation. In warn mode (the default) unknown types and attributes are allowed with a warning, because real campaigns and investigations get messy. In strict mode they are refused. Change it later with corkboard mode strict.

Every command that works on a workspace accepts -w PATH, or reads CORKBOARD_WORKSPACE. New database migrations are applied automatically when a workspace is opened.

Entering data from the command line

Record ids are ULIDs (26 characters). Commands print ids when they create something; capture them in shell variables as in the quick start, or copy them from list and show output.

Entities

uv run corkboard entity add TYPE LABEL [--tags a,b] [-v TAGS]
uv run corkboard entity edit ID [--type T] [--label L] [--tags a,b] [-v TAGS]
uv run corkboard entity list [--type T] [--tag TAG] [--as VIEWER]
uv run corkboard entity show ID [--as VIEWER] [--rating-mode strongest|weakest]
uv run corkboard entity delete ID [--yes]

label is only a display name. For facts about names (full name, aliases), use claims, so they carry sources and visibility. This lets an OSINT subject be labeled "Subject 1" while the real name stays a restricted claim.

Claims

uv run corkboard claim add ENTITY_ID KEY VALUE [--type TYPE] \
    [--valid-from DATE] [--valid-from-text TEXT] [--valid-to DATE] [--valid-to-text TEXT] \
    [--observed-at TIMESTAMP] [-v TAGS] [--source SOURCE_ID] [-c 1-6] [--excerpt TEXT]
uv run corkboard claim edit CLAIM_ID [--key K] [--value V] [--type T] [--clear-valid-from] ...
uv run corkboard claim delete CLAIM_ID [--yes]
  • Value types: string (default), int, float, bool, date, url, email, phone, geo (lat,lon). Values are checked and normalized; phone numbers lose spacing and punctuation so identical numbers match.
  • Dates: --valid-from accepts 2019, 2019-03, 2019-03-04, or a full timestamp. Anything else (like 3 Harvestmoon 1042 AR) is stored as text. --valid-from-text keeps the original wording alongside a sortable date: --valid-from 2019 --valid-from-text "circa 2019".
  • Adding with --source creates the claim and its evidence together. If the source is gm-canon and you give no -v, the claim is visible to gm only, so GM secrets never leak by omission.
  • Edits are for corrections. New information that disagrees with an existing claim should be added as another claim, so both stay visible with their sources.

Relationships

uv run corkboard rel add FROM_ID TO_ID TYPE [--directed|--undirected] [--attr key=value ...] \
    [--valid-from ...] [-v TAGS] [--source SOURCE_ID] [-c 1-6] [--excerpt TEXT]
uv run corkboard rel edit REL_ID [--type T] [--attr key=value ...] [--clear-attrs] ...
uv run corkboard rel list [--entity ID] [--type T] [--as VIEWER]
uv run corkboard rel delete REL_ID [--yes]

Direction defaults to what the schema pack says (for example, allied_with is undirected). Attribute values that are valid JSON keep their type: --attr years=3 stores a number.

Sources and evidence

uv run corkboard source add KIND TITLE [--uri URI] [--captured-at TS] [--sha256 HEX] \
    [--archive-path PATH] [--notes TEXT] [-r A-F] [--reliability-notes TEXT] [--derived-from ID ...] [-v TAGS]
uv run corkboard source edit ID [...]            # pass "" to clear a field
uv run corkboard source list [--as VIEWER]
uv run corkboard source show ID [--as VIEWER]
uv run corkboard source derive SOURCE_ID PARENT_ID [--remove]
uv run corkboard source delete ID [--yes]

uv run corkboard evidence add claim|relationship SUBJECT_ID SOURCE_ID [-c 1-6] [--excerpt TEXT] [-v TAGS]
uv run corkboard evidence list claim|relationship SUBJECT_ID
uv run corkboard evidence edit EVIDENCE_ID [--source ID] [-c 1-6] [--excerpt TEXT] [-v TAGS]
uv run corkboard evidence delete EVIDENCE_ID [--yes]
  • Source kinds are free text; common ones are url, document, session, interview, informant, and note.
  • Derived sources: if one article rewrites another (or a press release), record it with source derive. Sources that share an origin are never counted as independent corroboration.
  • A source cannot be deleted while evidence still refers to it.

Ratings: the Admiralty Code

corkboard rates information with the Admiralty Code (NATO system), which rates two things separately:

Reliability (the source) Credibility (the information)
A: Completely reliable 1: Confirmed by other sources
B: Usually reliable 2: Probably true
C: Fairly reliable 3: Possibly true
D: Not usually reliable 4: Doubtful
E: Unreliable 5: Improbable
F: Reliability cannot be judged 6: Truth cannot be judged
  • Reliability belongs to a source; credibility belongs to each evidence link. They are shown together as a code such as B2.
  • A claim with no evidence is rated F6 ("unsourced").
  • A claim with several evidence rows keeps all of them. Views show the strongest rating by default (credibility first, then reliability, so C2 ranks above B3), or the weakest with --rating-mode weakest.
  • Credibility 1 means independent corroboration. corkboard warns if you set it without two independent sources, and suggests it when evidence spans two or more independent source lineages. It never upgrades a rating on its own.

Visibility: GM secrets, handouts, and redaction

Every record (entity, claim, relationship, source, evidence) can carry visibility tags.

  • No tags means public. A tagged record is visible to viewers who hold at least one of its tags.
  • gm sees everything. Commands default to full access; use --as TAGS to see what someone else would see.
  • redacted shows the record as a [REDACTED] placeholder to viewers who cannot otherwise see it, instead of hiding it entirely. Redacted sources keep their reliability letter.
  • Per-character knowledge: tag records with names like pc:ana, then view as players,pc:ana to see exactly what that character knows.
  • A claim whose evidence is all hidden from a viewer looks unsourced to them, so they cannot tell that secret sources exist.
uv run corkboard claim add $MIRA secret "Informs for the Watch" -v gm
uv run corkboard entity show $MIRA --as players,pc:ana
uv run corkboard serve ~/campaigns/ashmoor --as players   # a locked, read-only player screen

The web board

uv run corkboard serve PATH [--port 8765] [--open] [--as TAGS]

The board runs at http://127.0.0.1:8765/ and is only reachable from your own machine. With --as players it becomes locked: read-only, fixed to that viewer, with no way to switch to GM view from the browser. That makes it safe to show on a shared screen.

Reading the graph

  • Node color follows entity type (or community, when chosen in the Analyze tab). Grey dashed nodes are redacted.
  • Edge style follows credibility: solid for 1 and 2, dashed for 3 and 4, dotted for 5, 6, or unsourced.
  • Edge opacity follows source reliability: fainter lines mean less reliable sources.
  • Hover a node or edge for its type and rating code. Click to inspect it; double-click a node to focus on it.

Left panel: filters and layouts

  • Filter by entity type, relationship type, tag, minimum reliability, maximum credibility, and a time window (YYYY, YYYY-MM, or YYYY-MM-DD). Undated and fictional-calendar records stay visible in time windows.
  • Edge rating switches between strongest and weakest ratings.
  • Hide unconnected entities and Show edge labels tidy the view.
  • Layouts: drag nodes where you want them and save named arrangements. Saving from a filtered view never discards positions of entities that view could not see.
  • Recently deleted: restore deleted records (with everything deleted along with them).
  • The view (filters, viewer, focus, layout, tab) is kept in the page URL, so you can bookmark it.

Top bar

  • View as: see the board as full access (GM), public, a tag such as players, or custom tags.
  • Export: download the current view as JSON, GraphML, CSV, or an Obsidian vault (see Exporting).
  • New: create an entity or source, or import a file.

Right panel tabs

  • Inspector: every claim with all of its ratings and evidence, relationships (incoming ones use inverse labels such as "has member"), and source details. On a writable board with the right access you can edit, delete, add claims, relationships, and evidence, view history, and run plugins from the Enrich menu. When adding a relationship, pick the other entity from the list or with Pick on graph. New records default to the tags of the view you are in.
  • Analyze: the path finder, sizing and coloring the graph by analysis results, and pivot mode (see Analysis).
  • Timeline: claims and relationships in world time, grouped by year, with span bars. Hovering flashes the entities on the graph; clicking inspects. Text-only dates are listed as undated.
  • Reports: claims eligible for a credibility upgrade, claims resting on weak sources, and conflicting values. Review opens the record in the Inspector; the report itself never changes anything.
  • Review: plugin suggestions waiting for a decision, plus the plugin network log (see Enrichment plugins).

Editing rules on the board

  • A locked board never writes.
  • You can edit records you can see in full in the current view; redacted records cannot be edited.
  • Deleting, viewing history, restoring, importing, and plugin actions need the full-access view, because a restricted view cannot see everything those actions affect.

History and undo

Every edit and deletion keeps a snapshot of what was there before. One operation (such as deleting an entity with all its claims and relationships) forms one batch that can be restored together.

uv run corkboard history show claim CLAIM_ID      # earlier versions, oldest first
uv run corkboard history deleted                  # recent deletions not yet restored
uv run corkboard history restore ENTRY_ID         # revert an edit, or undo a deletion

Restores are logged too, so a restore can itself be undone. On the board, use History on any record and the Recently deleted list in the left panel.

Analysis

All analysis runs on what the chosen viewer can see, so hidden records never influence a result. Relationships are treated as undirected for paths, centrality, and communities.

uv run corkboard analyze path FROM TO [--weight hops|rating] [--alternatives N]
uv run corkboard analyze centrality [--measure degree|betweenness] [--weight hops|rating] [--top N]
uv run corkboard analyze communities [--weight hops|rating]
uv run corkboard analyze pivots [--key ATTRIBUTE ...] [--min N]
uv run corkboard analyze timeline [--entity E] [--from DATE] [--to DATE]
uv run corkboard analyze corroboration
uv run corkboard analyze conflicts

FROM, TO, and --entity accept an id or a unique label. Every command takes --as plus graph filters such as --type, --rel-type, --min-reliability, and --max-credibility.

  • Paths: --weight hops counts links. --weight rating prefers well-sourced links (an A1 link costs 1.0, an F6 link 4.75), so a longer chain of good sources can beat one unsourced shortcut.
  • Centrality and communities: find the most connected or most "in between" entities, and groups that cluster together. Community detection is deterministic.
  • Pivots: entities that share a value, even without a relationship between them. Email, phone, and URL claims always count (across keys); other attributes count when the schema pack marks them pivot: true or you pass --key. Two accounts with the same email, or two NPCs who share a secret patron, show up here.
  • Timeline: dated items in order, then undated (text-only) items.
  • Corroboration: claims whose evidence comes from two or more independent source lineages (candidates for credibility 1), and claims resting only on D, E, or F sources.
  • Conflicts: different values for attributes the pack marks single: true (such as dob) during overlapping time periods. A status that changes over time is not a conflict.

On the board, the Analyze tab offers the path finder (routes are highlighted on the graph), Size entities by degree or betweenness, Color entities by community, and Pivot mode, which adds temporary diamond-shaped hub nodes linking entities that share a value. Pivot hubs are never saved or editable.

Exporting

uv run corkboard export json     --out backup.json
uv run corkboard export graphml  --out graph.graphml
uv run corkboard export csv      --out csv-folder/
uv run corkboard export obsidian --out vault-folder/ --as players
Format Output Use it for
json One file Full-fidelity backups and moving a workspace; the only format that can be restored
graphml One file Gephi and other graph tools; nodes carry claims, edges carry rating codes
csv A folder of CSV files and a README Spreadsheets and scripts
obsidian A vault folder One note per entity and per source, linked with [[wikilinks]], plus an index
  • Visibility-filtered exports: add --as players (or any tags) for a player handout or a client-safe report. Hidden records are left out and redacted ones appear as [REDACTED]. Filters such as --type, --tag, and --from/--to narrow an export further.
  • Complete vs partial: only a full-access export with no filters is complete, and only complete exports include history. Anything else is marked partial.
  • Safety: exports refuse to overwrite an existing file or fill a non-empty folder unless you pass --force. CSV cells that a spreadsheet would run as a formula get a leading apostrophe (turn off with --raw-csv). Obsidian notes escape HTML, so text copied from the web cannot trigger network requests when a note is opened, and source URLs are written as text rather than links.
  • Exported files live outside the workspace. Treat them with the same care as the workspace itself.

On the board, the Export menu downloads the current view (with the current filters, unless you untick filters). A locked player board exports only what the players can see.

Importing and restoring

Restore a JSON export

uv run corkboard restore backup.json ~/campaigns/ashmoor-copy

Restore always creates a new workspace; it never merges into an existing one. A complete export restores exactly, with the same ids, history, and layouts. A partial export (such as a player handout) restores with warnings about what it did not contain.

Import CSV or JSON with a mapping

A mapping file describes how each row becomes entities, claims, and relationships. Start from a generated mapping:

uv run corkboard import-starter staff.csv > staff-mapping.yaml
# edit staff-mapping.yaml, then:
uv run corkboard import staff.csv --mapping staff-mapping.yaml --dry-run
uv run corkboard import staff.csv --mapping staff-mapping.yaml

An example mapping for a CSV with columns first,last,email,company,title,born,conf:

source:                        # every import is backed by one source
  title: Harbor staff list     # default: the file name
  kind: document
  reliability: C
credibility: 4                 # default credibility for this import's evidence
credibility_column: conf       # optional per-row credibility (1 to 6)
visibility: [analyst]          # default visibility for created records
on_error: skip                 # skip bad rows, or abort (default) the whole import

entities:
  person:
    type: person
    label: "{first} {last}"    # templates use {column} placeholders
    match: [email]             # reuse an entity with the same email
    claims:
      full_name: "{first} {last}"
      email: {column: email, type: email}
      dob: {column: born, type: date}
  company:
    type: organization
    label: "{company}"
    match: [name]
    visibility: []             # override the default for this block
    claims:
      name: "{company}"

relationships:
  - {type: employed_by, from: person, to: company, attributes: {role: "{title}"}}

How imports behave:

  • Provenance is required. Each import creates one source (recording the file's sha256), or uses an existing one given as source: {id: SOURCE_ID}, and every created claim and relationship gets evidence from it.
  • Matching only happens when you ask. Without match, every row creates new entities. With match, rows sharing those values share one entity, and an existing entity is reused when exactly one matches.
  • No duplicates. Importing the same fact again adds corroborating evidence instead of a duplicate claim or relationship.
  • All or nothing. An import is one transaction. With on_error: skip, each bad row is rolled back cleanly and reported; otherwise the first bad row aborts everything and names the row. Strict validation always aborts.
  • Dry runs are exact. --dry-run performs the whole import and rolls it back, so the preview matches what the import would do.
  • JSON input: records may be a top-level list, a single list inside an object, or a path given with records: data.items. Nested fields use dotted column names like contact.email.

On the board, use New > Import file... to upload a file, generate or load a mapping, preview, and import. The board accepts files up to 10 MB; use the CLI for larger ones.

Import a graph file

A graph file (corkboard-graph format) lists sources, entities, claims, and relationships directly, each claim and relationship with its evidence. It suits data that is not a table, such as notes. Refs inside the file ("mira", "note:NPCs/Mira Vell.md") tie the pieces together; the reserved source ref gm-canon means the campaign's canon source.

uv run corkboard import-graph ashmoor.graph.json --dry-run
uv run corkboard import-graph ashmoor.graph.json
uv run corkboard import-graph --schema        # JSON Schema of the format

The whole file is checked before anything is saved, and every problem is listed with its location, for example claims[3].entity: unknown entity ref 'mira2'. Entities with match reuse the one existing entity that has that claim, sources with match_uri reuse a source with the same URI, and identical claims and relationships receive evidence instead of duplicating, so importing the same file twice adds nothing. The full format is documented in .claude/skills/obsidian-to-corkboard/reference/format.md.

Importing an Obsidian vault

Claude can read an Obsidian vault and write a graph file for you. It never runs corkboard or changes the vault: it surveys the notes, proposes a conversion plan, writes the file once you confirm, and checks it. You then run the import yourself.

The plan gives every note field a destination, so the import comes out clean:

  • fields that match pack attributes become claims;

  • fields that name other things (faction: MetaCorp, location: Watson) become relationships, with a small "stub" entity for names that have no note of their own (placeholders like "Independent" or "Unknown" are skipped);

  • workflow markers (status: active, potential, finished) become tags such as status:active, while in-world states (deceased, missing) stay claims;

  • clipped web pages become url sources rather than source_url claims;

  • campaign-specific facts that no built-in pack covers (a threat_level, say) go in a small campaign pack that Claude writes next to the import file for you to add (see Custom packs).

  • Claude Code: in this repository, ask something like "convert my Obsidian vault at ~/vaults/ashmoor for corkboard". The obsidian-to-corkboard skill in .claude/skills/ handles it.

  • Claude Cowork: install the skill once (see below), then work on the vault folder from Cowork.

  • claude.ai or a Project: paste the prompt from docs/prompts/obsidian-to-corkboard.md and attach your notes with their vault paths.

Using the skill with Claude Cowork

Cowork runs in the Claude desktop app and can work with folders on your computer, so it can read the whole vault the same way Claude Code does. You don't need corkboard or this repository on the machine Cowork uses; the skill only reads notes and writes a JSON file.

  1. Turn on skills. In Claude's settings, under Capabilities, enable "Code execution and file creation" (on Team and Enterprise plans, an owner enables skills for the organization).

  2. Package the skill. Zip the skill folder so the zip contains the obsidian-to-corkboard/ folder itself, not just its contents:

    cd .claude/skills
    python3 -m zipfile -c obsidian-to-corkboard.zip obsidian-to-corkboard/
    

    (On macOS you can also right-click the obsidian-to-corkboard folder in Finder and choose Compress.) Don't commit the zip; rebuild it after you pull changes to the skill.

  3. Upload it. In the Claude desktop app, go to Customize > Skills, click +, choose Create skill, then Upload a skill, and select the zip. Make sure obsidian-to-corkboard is toggled on. Uploaded skills are private to your account.

  4. Give Cowork the folders. Start a Cowork task and choose a folder that contains your vault and has room for the output file. A parent folder works well (for example ~/Documents, holding vaults/ashmoor/), so the file can be written next to the vault instead of inside it.

  5. Ask for the conversion. For example: "Convert my Obsidian vault in vaults/ashmoor into a corkboard import file at ashmoor.graph.json. The workspace is an RPG campaign using rpg-core." Claude recognizes the task and loads the skill (you can also pick it by typing / in the prompt box). It surveys the notes, asks you to confirm a conversion plan, writes the file, and checks it.

  6. Import on your machine with the commands below. If the dry run reports problems, paste them into the same Cowork task and ask Claude to fix the file.

Cowork never needs write access to the vault, and the skill tells Claude not to change it. Large vaults may take a while; the skill may offer to write a small Python script to process them, and you can say no.

Then:

uv run corkboard init ~/campaigns/ashmoor-test --kind rpg --packs rpg-core   # try a fresh workspace first
uv run corkboard packs sync -w ~/campaigns/ashmoor-test                      # existing workspaces: latest built-in packs
uv run corkboard packs add ashmoor-pack.yaml -w ~/campaigns/ashmoor-test     # only if Claude wrote a campaign pack
uv run corkboard import-graph ashmoor.graph.json --dry-run -w ~/campaigns/ashmoor-test
uv run corkboard import-graph ashmoor.graph.json -w ~/campaigns/ashmoor-test

If the dry run lists problems or warnings, paste them back to Claude to fix the file. A clean conversion has no warnings. Each note becomes a source with an obsidian:// link, and each note entity carries an obsidian_note claim with its vault path (stub entities match on their name), so importing an updated vault later reuses the same entities. A small example vault and its expected import file are in .claude/skills/obsidian-to-corkboard/examples/.

Enrichment plugins

Plugins look at an entity and propose new information. They are off by default, enabled per workspace, and everything they produce waits for your review.

uv run corkboard plugins available
uv run corkboard plugins enable email-domains
uv run corkboard plugins run email-domains --entity "@harborfox"     # or --type account
uv run corkboard suggestions list
uv run corkboard suggestions accept SUGGESTION_ID ...                # or --run RUN_ID
uv run corkboard suggestions reject SUGGESTION_ID ...
uv run corkboard plugins runs
uv run corkboard plugins log
uv run corkboard plugins disable email-domains

Built-in plugins, both offline:

  • email-domains: proposes a domain entity and an at_domain relationship for each email address on an entity.
  • url-hosts: does the same for the hosts of URL claims.

How review works:

  • Accepted items are backed by a source describing the plugin run (reliability F until you rate it) and inherit the visibility of the entity the plugin ran on.
  • Proposals that repeat what the workspace already knows, or what is already waiting for review, are skipped.
  • Accepting a relationship to a proposed entity accepts that entity too. Rejecting a proposed entity rejects the items that depend on it.

On the board, use Enrich... in an entity's inspector, then the Review tab to accept or reject.

Network plugins

Third-party plugins can be installed as Python packages. A plugin that needs the network must declare every host it contacts:

uv run corkboard plugins enable some-lookup --allow-network
# shows the hosts, warns that queries reveal what you are investigating,
# and asks you to type the plugin name to confirm
  • All plugin requests go through corkboard's own HTTPS client, limited to the declared hosts, with timeouts and size limits.
  • Every request, including refused ones, is recorded in corkboard plugins log (and the board's network log) with the full URL.
  • Set CORKBOARD_OFFLINE=1 to block all plugin network access.
  • Enabling plugins is only possible from the command line.

Plugin authors: see the plugin section in CLAUDE.md and src/corkboard/plugins/api.py for the interface.

Schema packs

Schema packs are YAML files that define entity types, expected attributes, relationship types, and inverse labels. corkboard ships with two:

  • rpg-core: pc, npc, faction, location, item, creature, plot_hook, event, deity, rumor, and relationships such as member_of, serves, allied_with, knows_about.
  • osint-core: person, organization, account, domain, email, phone, address, ip_address, event, vehicle, document, and relationships such as employed_by, uses_email, has_account, at_domain.

Every type in a pack also accepts the pack's common attributes (summary and obsidian_note in both built-in packs). Attributes can be marked pivot: true (shared values link entities in pivot analysis) or single: true (an entity should hold one value at a time, so differing values are conflicts):

entity_types:
  person:
    attributes:
      - {name: full_name, single: true}
      - {name: alias, pivot: true}
      - occupation
uv run corkboard packs available
uv run corkboard packs list      # packs stored in this workspace, and whether newer versions exist
uv run corkboard packs sync      # update this workspace's copies to the built-in versions

Each workspace keeps its own copy of its packs, so updating corkboard never silently changes how an existing workspace validates. Run packs sync when you want the newer definitions.

Custom packs

Add your own pack when a campaign or case needs fields or types the built-in packs lack. A custom pack merges with the others: it can add attributes to existing types, add new entity and relationship types, and declare common attributes.

name: nightcity-campaign        # any name except a built-in pack's
version: 1
common_attributes: [session_number]
entity_types:
  npc:
    attributes:
      - {name: threat_level, single: true}   # added to rpg-core's npc
  vehicle:
    attributes: [model, {name: plate, pivot: true}]
relationship_types:
  drives: {from: [pc, npc], to: [vehicle], inverse_label: driven by}
uv run corkboard packs add nightcity-pack.yaml    # add, or replace a pack with the same name
uv run corkboard packs remove nightcity-campaign  # records stay; only validation changes

packs sync leaves custom packs alone; edit the file, bump its version, and run packs add again. Custom packs are stored in the workspace and travel with JSON exports and restores.

Privacy and security

  • Local only. The board listens on 127.0.0.1 and accepts only local host names, which also blocks DNS-rebinding attacks from other websites. It sends a strict Content Security Policy and loads no outside resources.
  • Writes are guarded. Board write requests require a custom header that other websites cannot send, so a web page you visit cannot change your workspace.
  • No network by default. The only code that can make network requests is the plugin HTTP client, and only for plugins you enable with --allow-network.
  • Your data stays in the workspace directory unless you export it. Workspaces are not encrypted by corkboard; use disk or folder encryption for sensitive investigations.

Development

uv sync                                   # install with dev tools
uv run pytest                             # run the tests
uv run ruff check . && uv run ruff format .
uv run pyright                            # strict type checking

The source lives in src/corkboard/:

Package Contents
core/ Models, ratings, visibility, repository, history, graph views, matching
storage/ SQLite connection, migrations, all SQL queries
packs/ Schema pack loader and the built-in packs
analysis/ Paths, centrality, communities, pivots, timeline, reports
interchange/ Exports, mapped import, restore
plugins/ Plugin API, registry, runner, review queue, network client, built-ins
cli/ The corkboard command line
web/ The FastAPI server and the board (vanilla JavaScript, vendored Cytoscape.js)

Design rules, data model details, and conventions for contributors are in CLAUDE.md. Test fixtures use fictional data only.