- Python 82.8%
- JavaScript 14.7%
- CSS 1.5%
- HTML 1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .claude/skills/obsidian-to-corkboard | ||
| docs/prompts | ||
| src/corkboard | ||
| tests | ||
| .gitignore | ||
| .python-version | ||
| CLAUDE.md | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
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
- Installation
- Quick start
- Workspaces
- Entering data from the command line
- Ratings: the Admiralty Code
- Visibility: GM secrets, handouts, and redaction
- The web board
- History and undo
- Analysis
- Exporting
- Importing and restoring
- Enrichment plugins
- Schema packs
- Privacy and security
- Development
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 rpgalso creates a built-in gm-canon source (reliability A) for ground truth the GM enters directly.--packschooses which schema packs define the entity and relationship types.--modesets validation. Inwarnmode (the default) unknown types and attributes are allowed with a warning, because real campaigns and investigations get messy. Instrictmode they are refused. Change it later withcorkboard 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-fromaccepts2019,2019-03,2019-03-04, or a full timestamp. Anything else (like3 Harvestmoon 1042 AR) is stored as text.--valid-from-textkeeps the original wording alongside a sortable date:--valid-from 2019 --valid-from-text "circa 2019". - Adding with
--sourcecreates the claim and its evidence together. If the source is gm-canon and you give no-v, the claim is visible togmonly, 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, andnote. - 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
C2ranks aboveB3), 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.
gmsees everything. Commands default to full access; use--as TAGSto see what someone else would see.redactedshows 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 asplayers,pc:anato 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, orYYYY-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 hopscounts links.--weight ratingprefers well-sourced links (anA1link costs 1.0, anF6link 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: trueor 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 asdob) 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/--tonarrow 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. Withmatch, 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-runperforms 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 likecontact.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 asstatus:active, while in-world states (deceased,missing) stay claims; -
clipped web pages become
urlsources rather thansource_urlclaims; -
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-corkboardskill 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.
-
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).
-
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-corkboardfolder in Finder and choose Compress.) Don't commit the zip; rebuild it after you pull changes to the skill. -
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-corkboardis toggled on. Uploaded skills are private to your account. -
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, holdingvaults/ashmoor/), so the file can be written next to the vault instead of inside it. -
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. -
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
domainentity and anat_domainrelationship 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=1to 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 asmember_of,serves,allied_with,knows_about. - osint-core:
person,organization,account,domain,email,phone,address,ip_address,event,vehicle,document, and relationships such asemployed_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.1and 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.