anki-cli
Hybrid Anki CLI for humans and agents.
anki-cli supports both:
- AnkiConnect backend (use a running Anki Desktop instance)
- Direct SQLite backend (operate directly on collection files)
Alpha software: see the PyPI badge above for the current release.
Install
From PyPI (recommended):
uv tool install anki-cli
anki --version
With optional TUI extras:
uv tool install "anki-cli[tui]"
Quick Start
Inspect environment and backend:
anki status
anki version
status always exits 0 — it reports probe results in data.ok/data.error
rather than failing, so it works as a health check on hosts with no Anki.
List core entities:
anki decks
anki notetypes
anki tags
Query cards and notes:
anki cards --query "deck:Default is:due" # full card details (first 1000; --limit 0 for all)
anki cards:ids --query "deck:Default is:due" # ids only
anki notes --query "tag:verb" # note ids
anki browse --query "tag:verb" # interactive TUI
Inspect individual objects:
anki card --id 1234567890
anki note --id 1234567890
anki deck --deck "Default"
anki notetype --notetype "Basic"
Backend Modes
Global backend selection:
anki --backend auto ...
anki --backend ankiconnect ...
anki --backend direct ...
Collection override (direct backend):
anki --backend direct --col "/path/to/collection.anki2" status
Anki profile selection (for multi-profile installs):
# ~/.config/anki-cli/config.toml
[collection]
anki_profile = "Work"
When set, collection discovery picks <Anki2>/<anki_profile>/collection.anki2
instead of the first profile found. A name matching no profile exits with code
3 and lists the available profiles — but only where a local collection is
actually required: on the AnkiConnect path the lookup is informational and a
miss never fails. --col takes precedence over it entirely.
Backend behavior:
auto: detects and chooses best available backendankiconnect: forwards search queries tofindCardsandfindNotesdirect: compiles queries to SQL and executes directly on the collection DB
The old standalone backend was removed: prefer = "standalone" in
config.toml and ANKI_CLI_BACKEND=standalone warn and run as auto;
--backend standalone is rejected at the CLI with a usage error (exit 2).
Remote AnkiConnect
To connect to AnkiConnect running on a different machine (e.g. via Tailscale or LAN):
# ~/.config/anki-cli/config.toml
[backend]
prefer = "ankiconnect"
ankiconnect_url = "http://192.168.1.100:8765"
allow_non_localhost = true
The remote Anki Desktop must have AnkiConnect configured to accept non-localhost connections. Set "webBindAddress" to a specific interface address (e.g. your Tailscale IP) or "0.0.0.0" for all interfaces. If binding to all interfaces, consider restricting access with firewall rules.
Without allow_non_localhost = true the remote URL is never contacted — the
detection probe and all data operations refuse non-localhost hosts.
Search Query Language
Filters follow Anki's own semantics:
deck:NAME— the deck and its subdecks (deck:LangcoversLang::Spanish), including cards visiting a filtered deck;deck:*(all),deck:filtered(cards in filtered decks); supports*glob. Name matching is case-insensitive for ASCII.notetype:NAME(or Anki's spellingnote:NAME)tag:NAME— the tag and its children (tag:verbcoversverb::irregular);tag:nonefor untagged notes,tag:*for every note; supports*globis:new/is:review(by card type, so a suspended new card is stillis:new),is:learn,is:due(learning/review cards whose due time has passed; never new cards),is:suspended,is:buriedadded:N— cards created in the last N scheduling days (added:1= since the last rollover)flag:Nprop:ivl>N,prop:due>N,prop:reps>N,prop:lapses>N(<,<=,=,>=,>)nid:ID,cid:ID- bare text and quoted text (
"specific text"); escape a literal colon as\:
Any other prefix: (for example card:, rated:, mid:, deck:current, field searches like
front:dog) is rejected with INVALID_INPUT rather than silently searched as text.
The --deck option on review, decks and deck follows the same rule: it covers the named deck
and its subdecks, so a parent row in decks includes its children's counts.
Logical syntax:
- implicit
ANDfrom whitespace - explicit
OR - unary
-andNOT - parentheses for grouping
Examples:
anki cards --query "deck:Japanese is:due"
anki cards --query "tag:verb -is:suspended"
anki notes --query "\"specific text\""
anki cards --query "(tag:a OR tag:b) is:new"
Common Commands
Cards
anki cards --query "deck:Default" # details for every match
anki cards:ids --query "deck:Default" # ids only
anki card --id 123
anki card:suspend --query "is:due"
anki card:unsuspend --id 123
anki card:move --query "tag:to-move" --deck "Archive"
anki card:flag --query "is:review" --flag 3
anki card:bury --query "deck:Default" # direct backend only
anki card:unbury --deck "Default" # direct backend only
anki card:reschedule --query "tag:reset-me" --days 3
anki card:reset --query "tag:relearn"
anki card:revlog --id 123 --limit 20
Notes and tags
anki notes --query "deck:Default"
anki note --id 123
anki note:add --deck "Default" --notetype "Basic" --Front "Q" --Back "A"
anki note:add --deck "Default" --notetype "Basic" --Front "Q" --Back "A2" --allow-duplicate
anki note:edit --id 123 --Front "Updated Q" --Back "Updated A"
anki note:fields --id 123
anki note:delete --id 123 --yes
anki tag --tag "verb"
anki tag:add --query "deck:Default" --tag "important"
anki tag:remove --id 123 --tag "important"
anki tag:rename --from "old" --to "new"
Like Anki's own Add dialog, note:add refuses a note whose first field already exists in
the same notetype (exit 1) or is empty. --allow-duplicate lifts the duplicate check; it
also works on note:bulk, where refused items otherwise come back as null ids.
Decks and notetypes
anki deck --deck "Default"
anki deck:create --deck "Japanese::Vocab"
anki deck:rename --from "Old" --to "New"
anki deck:delete --deck "Temporary" --yes
anki deck:config --deck "Default"
anki deck:config:set --deck "Default" --new-per-day 20 --reviews-per-day 200
anki notetypes
anki notetype --notetype "Basic"
anki notetype:create --notetype "MyType" --field "Front" --field "Back"
anki notetype:field:add --notetype "Basic" --field "Extra"
anki --yes notetype:field:remove --notetype "Basic" --field "Extra" # deletes the field from every note
anki notetype:css --notetype "Basic" --set ".card { font-size: 18px; }"
Review
anki review
anki review:next
anki review:show
anki review:preview --id 123
anki review:answer --id 123 --rating good
anki review:undo
Interactive TUI review (requires TUI extras, direct backend):
anki review:start --deck "Japanese"
Output and Exit Codes
Global output formats:
anki --format json ...
anki --format table ...
anki --format md ...
anki --format csv ...
anki --format plain ...
Every command names its target the same way: --deck, --notetype, --tag, --id,
--query; --field is repeatable. Older spellings (deck:create --name,
notetype:create --name, tag:add --tags, note:fields --fields a,b) still work as
aliases. anki commands --format json lists every command, option, exit and error code.
Global options (--format, --backend, --col, --yes, --copy, --no-color) may be
written before or after the subcommand: anki --yes note:delete --id 1 and
anki note:delete --id 1 --yes are the same command.
Every failure — including a mistyped option or command name — is reported through the
same envelope, so --format json always yields parseable stderr. Set ANKI_CLI_DEBUG=1
to append a Python traceback after an INTERNAL_ERROR envelope.
Exit codes:
0: Success1: Backend operation failed (including unexpected errors)2: Invalid input, usage error, confirmation required, or unsupported operation3: No Anki backend found (auto mode could not reach AnkiConnect or find a collection)4: Entity not found7: Backend unavailable or collection locked130: Interrupted (Ctrl-C)
status is the exception: it reports detection failures as
{"ok": true, "data": {"ok": false, "error": "..."}} and still exits 0.
AI Agent Integration
This repo ships a SKILL.md at the project root — an agent skill that AI coding agents can consume to operate anki-cli without human guidance. It covers the full command reference, JSON output structure, search query syntax, error codes, and common workflows.
Any supported agent (recommended)
npx skills add ubermenchh/anki-cli
Automatically detects your installed agents and places the skill in the correct directory. Works with Claude Code, Codex, Cursor, Copilot, OpenCode, and others.
Manual
Copy or symlink SKILL.md into your agent's skills directory:
| Agent | Skill directory |
|---|---|
| Claude Code | ~/.claude/skills/anki-cli/SKILL.md |
| Codex | ~/.codex/skills/anki-cli/SKILL.md |
| Cursor | ~/.cursor/skills/anki-cli/SKILL.md |
| OpenCode | ~/.agents/skills/anki-cli/SKILL.md |
| Project-level (any) | .agents/skills/anki-cli/SKILL.md |
mkdir -p ~/.claude/skills/anki-cli
cp SKILL.md ~/.claude/skills/anki-cli/SKILL.md
For other AI coding agents, point them at SKILL.md in the repo root or include it in your agent's context/system prompt.
Safety Notes
- Use
--yesfor destructive operations (note:delete,deck:delete,notetype:field:remove). - In direct mode, avoid write operations while Anki Desktop is open.
- If Anki Desktop is running, prefer
--backend ankiconnect. review:undo(direct mode only) restores the card's previous state and deletes the revlog row written by the undone answer, matching Anki's own undo.- In direct mode,
csumfor notes with HTML in the first field now matches Anki. Rows written by older anki-cli versions keep their old csum until Tools ▸ Check Database recomputes them; duplicate detection may miss them until then.
Development
uv sync --group dev
uv run ruff check .
uv run ty check
uv run pytest
With optional TUI dependencies:
uv sync --group dev --extra tui
uv run pytest -m tui
Adding a command
Commands live in anki_cli/cli/commands/ and are declared with @anki_command
(anki_cli/cli/command.py), which owns the output envelope, the backend session, and
BACKEND_UNAVAILABLE. The body returns the success data and declares which other
exceptions become which error code:
@anki_command("deck", errors={LookupError: ("ENTITY_NOT_FOUND", 4)})
@click.option("--deck", "deck_name", required=True)
def deck_cmd(cmd: CommandContext, deck_name: str) -> JSONValue:
name = deck_name.strip()
if not name:
raise cmd.invalid("Deck name cannot be empty.")
with cmd.errors(details={"deck": name}):
return cmd.backend.get_deck(name)
Anything not mapped falls through to the entry-point mapper, which still renders an
envelope. anki commands --format json lists the result; the docs-sync tests check that
README and SKILL.md only mention commands and options that exist.
License
MIT. See LICENSE.
Metadata
Release files for anki-cli 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| anki_cli-0.2.0.tar.gz | 594.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| anki_cli-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 780.6 kB
Release files / anki_cli-0.2.0.tar.gz
| Download URL | anki_cli-0.2.0.tar.gz |
|---|---|
| Size | 594.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ffe9cdd6062e964183460d283728d4b44c449352741b7d03dc99560d122aea13
|
|
BLAKE2b-256 checksum How to use checksums |
096762b040b530365b1123c660b40df35806a3d8c7e9fd3d0218010cbe0044a8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / anki_cli-0.2.0-py3-none-any.whl
| Download URL | anki_cli-0.2.0-py3-none-any.whl |
|---|---|
| Size | 186.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
11cd8481be05f3df39805e30dea21f8aed72d4a396803435f2ae6f700b0b992a
|
|
BLAKE2b-256 checksum How to use checksums |
348924d7c5b512cd7987c55d36443949248a739aca2ce14105b2deb88f879603
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency log