onenote-cli
Use your OneNote notebooks from the terminal through the Windows desktop application, using its existing signed-in session. No Graph connection, Entra app registration, or separate OAuth consent is required by this backend.
Discover notebooks and sections, list and search pages, read page content, and
create or append plain-text notes. The interactive workspace opens with onenote tui;
regular commands offer terminal output, JSON when piped, bounded collections,
shell completions, and an offline CLI Spec contract.
Requirements
- Windows OneNote desktop with your notebooks already open and accessible.
- Windows PowerShell 5.1 (
powershell.exe) on the Windows machine. - Local access: run as the Windows user who uses OneNote. WSL can use Windows interop; OneNote still runs on the Windows side.
- Remote access: an OpenSSH client, key or agent authentication to Windows, and a signed-in Windows user with permission to register an interactive scheduled task. OneNote must be configured in that user’s desktop session.
OneNote for macOS and the browser do not expose this COM interface. The SSH backend lets macOS and Linux clients use OneNote on a Windows machine. Existing notebook permissions, password protection, and organizational desktop policies still apply.
Install
The package is named onenote-cli on both crates.io and PyPI. Both install the
onenote command:
uv tool install onenote-cli
# Or, with Rust installed:
cargo install onenote-cli --locked
The initial wheels support x86-64 Windows and x86-64 Linux (glibc 2.17 or later). Local Linux desktop access requires WSL with Windows interop; ordinary Linux can connect to a Windows machine through an SSH profile. macOS currently requires installation with Cargo. The wheel contains the native executable, so installing it needs no Rust compiler. To install a downloaded wheel:
uv tool install --no-build --no-index ./onenote_cli-0.1.3-py3-none-win_amd64.whl
If you installed the earlier onenote-cli-rs package, switch names with:
uv tool uninstall onenote-cli-rs
uv tool install onenote-cli
The original onenote-cli-rs 0.1.0 package remains available and unchanged.
Run uv tool update-shell if onenote is not on PATH, then open a new terminal.
For WSL2, install uv inside the Linux distribution and use its Linux wheel.
Check that powershell.exe is on PATH and that Windows OneNote is open under the
same Windows user. Run onenote doctor and onenote search 'test' --scan from
that WSL terminal. Linux wheel installation has been tested in a container;
live WSL2 access remains unverified because the kiosk reports virtualization
unavailable and WSL installation failed. Native Windows uv installation is
validated separately; this is not evidence of working WSL2 interop.
Setup and profiles
On the Windows machine, or in WSL with Windows interop:
onenote init
onenote notebooks list
To use a Windows desktop remotely, first establish key-based SSH access and verify its host key using your normal SSH client. Existing SSH aliases, agent keys, identity files, ports, and proxy configuration are supported. The CLI does not prompt for SSH passwords or accept unknown host keys automatically.
ssh kiosk
onenote init --profile kiosk --backend ssh --host kiosk
onenote --profile kiosk doctor
onenote notebooks list
Here kiosk is an alias in your SSH configuration. Without an alias, use
--host user@host, optionally --identity-file /path/to/key and --port 2222.
Only the identity file's absolute path is saved, never its contents.
init verifies OneNote access before saving and selects the new profile.
Use --no-check to prepare settings offline, --force to replace an existing
profile, and --read-only to block page creation and appends. Read-only profiles
still allow --dry-run previews, which include the selected connection.
onenote init --profile local
onenote profile list
onenote profile use kiosk
onenote config show
onenote config path
onenote profile remove local
Selection order is --profile NAME, then ONENOTE_PROFILE, then the saved
active profile. profile list marks the saved default and any override;
config show explains which selection is in effect. Profile removal deletes
only local settings. Removing the active profile requires selecting another;
it never silently switches machines. Existing installations without a config
file continue to use the local desktop.
Configuration lives in $XDG_CONFIG_HOME/onenote/config.toml, otherwise
~/.config/onenote/config.toml on Unix or %APPDATA%\onenote\config.toml on
Windows. ONENOTE_CONFIG overrides the entire file path, useful for automation
and isolated environments. Updates are atomic and locked against concurrent
writers; Unix config files are private to their owner.
How remote access works
The SSH backend sends the bundled PowerShell bridge and JSON request over the
encrypted connection. A temporary, limited-privilege Windows task runs it in
the SSH user's signed-in desktop session. It does not use Graph or require
an Entra application. The Windows SSH account must be the account using OneNote;
log into that desktop before running init or doctor.
Each request has a private temporary directory and a unique task name. Tasks
and request files are removed when the SSH command finishes. The bridge has a
45-second deadline; SSH commands allow up to 90 seconds including task startup.
If the SSH connection is forcibly terminated, the task retains its own deadline,
but files or task registration can remain under
%LOCALAPPDATA%\OneNoteCli\requests. These can be removed after the task stops.
Writes are never retried automatically. Interrupted writes return
write_uncertain; inspect the target page before trying again.
doctor --offline checks settings and the local transport executable without
opening a connection. It does not claim OneNote is available; use doctor for
a live check. SSH setup does not install OneNote, configure an SSH server, or
change your desktop session.
Build and start
Requires Rust 1.90 or later. From this directory:
cargo install --path . --locked
onenote init
onenote doctor
onenote notebooks list
onenote sections list --notebook '{NOTEBOOK-ID}'
onenote pages list --section '{SECTION-ID}'
onenote pages read '{PAGE-ID}'
onenote search 'quarterly review' --scan
Interactive workspace
Run onenote tui in a terminal to browse notebooks, sections, and pages with
the selected local or SSH profile. Enter drills into the selected item; Esc
goes back. At wider terminal sizes, the page text appears beside the list.
The workspace starts at your open notebooks and uses the same desktop session
and read-only setting as the other commands.
| Key | Action |
|---|---|
j / k, arrows |
Move through the list |
Enter, Esc |
Open an item or go back |
b |
Return to notebooks |
/, s |
Indexed search or literal text scan |
f, n, r |
Filter loaded items, load more, refresh |
PageUp / PageDown |
Scroll page text |
c, a |
Create a page in the open section or append to a selected page |
?, q |
Show keyboard help or quit |
Search results indicate when indexing is pending or a literal scan is
incomplete. The literal scan covers the first 100 candidate pages; use the
CLI's search --scan options for further batches. Create and append accept
plain text, keep an unfinished draft for the current session, and show a
review screen before writing. If OneNote does not confirm a write, the
workspace blocks further writes for that session so you can inspect the
desktop state before trying again. Write keys are disabled for read-only
profiles. The TUI requires a terminal; scripts should use the regular CLI.
Copy IDs from the preceding command. These are desktop OneNote IDs, not Graph IDs. Quote them in your shell. Sections inside section groups are included and identified by their hierarchy path. Recycle-bin content is excluded from lists.
onenote search 'budget AND review' --scope '{NOTEBOOK-ID}' --limit 10
onenote pages list --section '{SECTION-ID}' --limit 10 --offset 10
onenote pages list --section '{SECTION-ID}' --fields id,name --output json
onenote pages read '{PAGE-ID}' --xml --output json
onenote doctor --offline
onenote schema --command 'pages read'
onenote completions powershell
Collections return items, total, next_offset, and truncated. Reuse the same
command and filters with --offset set to next_offset; stop when it is null.
The default limit is 25, maximum 100. Pagination is positional: changes between
requests can shift items. OneNote supplies the hierarchy before local pagination;
the bridge response is capped at 32 MiB. Local commands time out after 45 seconds;
SSH commands allow 90 seconds including desktop task startup.
Page results include markdown, extracted from OneNote text blocks. This is not
a layout-preserving export: tables, tags, and outline nesting may lose structure;
ink, images, and attachment bytes are not exported. --xml includes the original
page XML without binary payloads. Reads reflect desktop state and do not force
cloud synchronization. Search uses OneNote's search syntax and requires the
Windows Search (WSearch) service to be running. It does not navigate the desktop
UI. Unindexed pages are excluded from matches: results include indexing_pending
and unindexed_count so an empty result cannot hide pending indexing. New or
changed pages may take time to become searchable. Listing and reading pages do
not depend on the search index.
Search without Windows indexing
onenote search 'quarterly review' --scan --scope '{SECTION-ID}'
onenote search 'quarterly review' --scan --scope '{NOTEBOOK-ID}' --scan-limit 100
onenote search 'quarterly review' --scan --scope '{NOTEBOOK-ID}' --scan-offset 100
--scan reads titles and page text directly, with case-insensitive literal
substring matching and whitespace normalization. It does not interpret Boolean
operators, wildcards, or OneNote search syntax. HTML formatting is removed and
entities decoded; link destinations, ink, OCR, and attachment content are not
searched. It needs no Windows Search service. Use a section scope for fast,
focused searches; without a scope it scans all open notebooks.
Each request attempts at most 100 pages by default (--scan-limit accepts 1–500).
A scan stops starting page reads after 20 seconds or when its XML response budget
reaches 4,194,304 UTF-16 code units (before JSON escaping). An individual blocked COM call can still hit
the bridge's 45-second timeout, which returns an error rather than partial results.
Oversized pages and read failures appear in skipped_pages; locked sections are
never read and are counted in skipped_locked_sections.
total counts matches in the current scan batch. --limit, --offset, and
next_offset paginate those matches. After consuming them, use
next_scan_offset as --scan-offset and reset --offset to zero to scan the next
batch. Keep the query, scope, and scan limit unchanged. These cursors are
positional, not snapshots; edits or timing differences between requests can shift
results. Narrow the scope if a scan cannot advance its cursor.
incomplete: true means there are more candidate pages, skipped pages, or locked
sections. next_scan_offset: null means no more candidates remain, but skipped
content can still make results incomplete. scanned_pages, attempted_pages,
and candidate_pages describe coverage; candidate counts exclude locked and
recycled pages. Partial results exit successfully with this metadata. Terminal
output also explicitly labels incomplete results.
--output auto selects text on a terminal and JSON when piped. Use -o text to
force readable text or -o json for automation. --no-color and NO_COLOR
disable color. Errors go to stderr as structured JSON in JSON mode, leaving
stdout empty. Exit codes: 1 local failure, 2 invalid input/unsupported platform,
4 resource not found, 5 desktop bridge failure, 6 write conflict, 7 uncertain write. OneNote COM failures, including
inaccessible IDs and locked sections, are reported as desktop errors with the
original diagnostic. Locked-section errors also explain how to unlock the section
in the desktop application.
Create and append
onenote pages create --section '{SECTION-ID}' --title 'Meeting notes' --file notes.txt --dry-run
onenote pages create --section '{SECTION-ID}' --title 'Meeting notes' --file notes.txt
onenote pages append '{PAGE-ID}' --text 'Follow up on the proposal.'
cat notes.txt | onenote pages append '{PAGE-ID}' --file -
Supply exactly one of --text or --file. Content is UTF-8 plain text, limited
to 1 MiB; titles are limited to 512 bytes. Markdown and HTML are treated as
literal text. Input line breaks become separate OneNote text elements. A UTF-8
BOM is removed and CRLF/CR line endings are normalized. --dry-run works offline
and prints the request without checking whether the target exists or is writable.
Create and append are explicit write commands and do not ask for confirmation.
Append places a new outline below existing content, without sending existing
outlines, images, or attachments back to OneNote. Every update uses the page's
last-modified timestamp and force=false. A concurrent edit returns conflict
(exit 6) rather than overwriting content.
Success includes the page ID, the resulting Markdown, operation, and
verified: true. Verification reads the page back, checks the new text (ignoring
rendering whitespace differences), and checks that existing page-level object IDs
remain. Create also checks the title. This verifies desktop state, not cloud sync.
These operations are not idempotent: repeating a successful command creates
another page or appends another block. Creation is a two-step desktop operation;
a failure after creating the page can leave a blank or partially populated page.
A timeout, interrupted response, or failed verification returns write_uncertain
(exit 7). Inspect OneNote before retrying. When known, the error includes
page_id for recovery. The CLI never retries a write or automatically deletes a
partially created page.
Implementation and validation
The Rust CLI passes JSON over stdin to a bundled PowerShell script. User text is
never interpolated into executable code. Read commands use GetHierarchy,
FindPages, and GetPageContent. The separate write bridge adds CreateNewPage
and UpdatePageContent. Both release their COM reference and never quit the
user's OneNote application.
cargo fmt -- --check
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked --all-targets
On Windows, tests exercise the production script using a mock COM object. On other platforms with PowerShell installed:
ONENOTE_TEST_POWERSHELL=/path/to/pwsh cargo test --test desktop_bridge --test desktop_write -- --ignored
Mock tests cannot verify a real OneNote installation. Before release, run
doctor, notebook/section/page discovery, search, and page reading against a
Windows desktop profile, including a nested section and a locked section.
The initial kiosk smoke test passed discovery, nested sections, pagination,
field projection, Unicode page reads, Markdown/XML output, and pending-index
search behavior. Live create/append checks also passed for Unicode, literal
special characters, line breaks, existing-content preservation, and dry-run.
A real password-locked section was verified: discovery reports locked: true,
page listing requests an unlock, and creation is refused before mutation.
Page read/append errors for locked content are covered by COM-error fixtures.
Live --scan checks passed positive title and body matches, Unicode, negative
queries, bounded continuation, and locked-section omission with incomplete-result
metadata.
Indexed search remains an environment limitation: the kiosk's OneNote
crawler currently fails with 0x80004005 despite a running Windows Search service
and a targeted re-crawl. A normal OneNote restart and Office Quick Repair also
completed without resolving the crawler failure. A full kiosk reboot cleared
pending Windows servicing but did not fix indexing. Post-reboot checks passed
doctor, scratch-page discovery and reading, and confirmed that LockedSmoke
remains locked. Windows Search now uses delayed automatic startup on the test
kiosk. It must remain
enabled in the kiosk configuration; a provisioning run that disables WSearch
will break search again.
Microsoft references: Application interface and COM enumerations.
Prepare a release locally
The initial standalone package targets x86-64 Windows. On Windows, build and package it with:
cargo build --release --locked --target x86_64-pc-windows-msvc
python scripts/package.py --target x86_64-pc-windows-msvc --output dist
Cross-builds with cargo zigbuild --release --locked --target x86_64-pc-windows-gnu can be packaged with the corresponding GNU target.
The script verifies the executable architecture and archive contents, includes
this README, the license, and changelog, and writes a SHA-256 sidecar. Extract the
archive and verify onenote.exe --version, doctor, and the live read/write
smoke checks on Windows before publishing. Packaging does not publish or tag.
Registry packages are built with Maturin's binary bindings. Build the Windows
wheel on Windows or the Linux wheel on Linux; maturin build --release --locked --out dist uses the current platform. For a portable Linux wheel use the
manylinux build in .github/workflows/release.yml.
The release workflow tests wheel installation with uv tool install --no-build --no-index, validates the Windows/Linux wheel matrix and both source packages,
and creates SHA256SUMS. A tag builds packages but does not publish. Publication
requires an explicit workflow dispatch on that version tag with publish=true
and repository secrets CARGO_REGISTRY_TOKEN and PYPI_API_TOKEN.
Maintainers must configure those secrets and confirm registry ownership before publishing. If a registry publication partially succeeds, preserve that version and tag and use a patch release for further changes; do not overwrite already published artifacts.