mark
Privacy-first CLI bookmark manager — SQLite + FTS5 full-text search
v1.0.1Quick Start
Install via jcli (recommended)
jcli install mark
Or via curl + bash
curl -fsSL https://cli.johlem.net/tools/mark/install.sh | bash
First-run smoke test
mark init
mark add https://doc.rust-lang.org/book/ -t "The Rust Book" -T rust,docs --rate 5 --pin
mark add https://attack.mitre.org -t "MITRE ATT&CK" -T security -C pentest
mark ls
mark search "rust"
mark stats
mark export --format json --pretty
What it does
mark is a privacy-first bookmark manager that lives entirely in your terminal.
Bookmarks sit in a single SQLite file under ~/.local/share/mark/, indexed by
FTS5 for instant full-text search across URL, title, description, and notes. No browser
extension, no daemon, no cloud account — just one binary that pipes cleanly into
fzf, rofi, xargs, and the rest of your shell.
- SQLite + FTS5. One file, full-text BM25 ranking, scales to tens of thousands of bookmarks without breaking a sweat. External-content FTS5 means the search index stays consistent with the source table via triggers.
- URL normalization. Adds collapse onto a single row regardless of
www., trailing slashes, query-parameter ordering, or tracking junk (utm_*,fbclid,gclid,mc_*,ref_,yclid,msclkid). - XDG-compliant. Honours
$XDG_CONFIG_HOMEand$XDG_DATA_HOME. Defaults to~/.config/mark/and~/.local/share/mark/. - Recoverable. Timestamped
VACUUM INTObackups, JSON and Markdown exports, Netscape HTML round-trip.--max-backupsprunes the oldest.dbsnapshots automatically. - Pipe-clean output.
--url-onlyfor shell pipelines,--jsonfor jq, plain text rows by default. No ANSI noise unless you ask for it. - Offline-first. Zero network calls in v1.0. Network-dependent
features (
mark check,mark archive,mark refresh) are explicitly deferred to v1.1.
Subcommands
| Command | What it does |
|---|---|
mark add <URL> | Save a bookmark. -t TITLE -T tags -n NOTE -C COL --pin --rate 1..5. URL is normalized before insert; duplicates merge. |
mark rm <ID|URL> | Remove by numeric ID, exact URL, or unique FTS hit. -f skips the prompt. |
mark edit <ID|URL> | Update fields. --tags replaces; --tags-add / --tags-remove mutate incrementally. |
mark ls | List with filters: tag (repeatable, AND), collection, domain, pinned, dead, min rating. --sort created|updated|rating|title. |
mark search <QUERY> | FTS5 BM25 search across URL / title / description / notes. Supports phrases, OR, -exclude. |
mark open <ID|QUERY> | Launch URL via xdg-open, or --print to stdout. |
mark copy <ID|QUERY> | Copy URL to the system clipboard. |
mark pin / mark unpin / mark tag | Single-field mutations. |
mark recent | Newest N bookmarks; defaults to 10. |
mark stats | Totals, top tags, top domains, dead-link percentage, oldest / newest. |
mark domains | Every unique domain with counts. --alpha for alphabetical. |
mark backup [--format db|json|md] | Timestamped snapshot. .db uses SQLite VACUUM INTO; .json / .md are portable exports. |
mark restore <FILE> | Restore from a backup. .db requires --replace; .json merges by default. |
mark import <FILE> | Netscape HTML (Firefox / Chrome / Safari / Pocket), native JSON, or CSV. |
mark export | JSON / Markdown / HTML (Netscape, re-importable) / CSV. |
mark init | Write default config and create the database directory. Idempotent. |
mark completions <SHELL> | bash / zsh / fish / elvish / powershell completion script. |
Shell integration
# Fuzzy-open via fzf
mark ls --url-only | fzf | xargs xdg-open
# Filter by tag, pipe URLs into curl
mark search "privilege escalation" --tag oscp --url-only \
| while read url; do curl -sI "$url"; done
# Rofi launcher (bind to a key in i3 / sway)
mark ls --json | jq -r '.bookmarks[] | "\(.id)\t\(.title // .url)\t\(.url)"' \
| rofi -dmenu -i -p "bookmarks" \
| awk -F'\t' '{print $1}' \
| xargs mark open
# Pipe clipboard URL into mark
xclip -o | xargs mark add -T inbox
# Suggested aliases
alias mb="mark ls --url-only | fzf | xargs -I{} mark open {}"
alias mba="mark add"
alias mbs="mark search"
alias mbl="mark ls --limit 20"
Configuration
Path: ~/.config/mark/config.toml (XDG). All sections optional;
defaults are applied for absent keys.
[database]
# path = "~/.local/share/mark/bookmarks.db"
[backup]
enabled = true
# dir = "~/.local/share/mark/backups"
max_backups = 30
[output]
default_limit = 50
Exit codes
| Code | Meaning |
|---|---|
0 | Ok |
2 | Usage error (clap) |
3 | Runtime error — DB I/O, parse failure, or bookmark not found |
Touches / Produces / Gates
- Touches (read+write): the SQLite database at
$XDG_DATA_HOME/mark/bookmarks.db(default~/.local/share/mark/bookmarks.db); the backup directory at$XDG_DATA_HOME/mark/backups/; the config file at$XDG_CONFIG_HOME/mark/config.toml(read-only at runtime, written bymark init). - Produces: stdout — text rows (default),
JSON (
--json), or URL-only lines (--url-only).backupwrites timestamped files to the backup directory.exportwrites to--outor stdout. - Gates: none.
markis offline by default in v1.0 — no network calls anywhere.--i-am-authorizedis not required.
Roadmap
- v1.0 (this release) — add / rm / edit / ls / search / open / copy / backup / restore / import / export / init / completions, plus URL normalization, XDG storage, and FTS5 search.
- v1.1 —
mark check(async dead-link audit),mark tui(ratatui),mark sync --rclone / --rsync,mark archive(Wayback),mark refresh,mark dupes, git-backed backups. - v1.2 —
ageencryption + YubiKey PIV at rest,urlrecon/phishprobehooks for security-aware collections, encrypted export bundles. - v2.0 — browser-extension messaging host, local read-only HTTP API
for rofi / external tools, AI-assisted tagging via local LLM,
mark similarvia embeddings.
Build from source
cd tools/mark/rust
cargo build --release # ./target/release/mark
cargo test # 6 unit + 5 integration tests
Toolchain pin: Rust 1.85.0. Runtime dependencies: glibc (kernel 3.2+).
Crate dependencies: clap, clap_complete, rusqlite
(bundled SQLite + FTS5), serde, serde_json, toml,
chrono, anyhow, thiserror, directories,
arboard, open, url.
Release artefacts & provenance
Every signed release ships with checksum + signature sidecars plus a CycloneDX SBOM. Enterprise procurement can pin third-party components without unpacking the binary.
-
manifest.json— pinned SHA-256 +.sigURL + SBOM URL per platform -
johlem-release.pub— Ed25519 public key used bysignify -V(fingerprint:4532510f…6748, pinned injcli verify) CHANGELOG.md— semver-tagged release notes- Security contact:
security@johlem.net