hermes-yandex-calendar
Let your Hermes Agent run your Yandex Calendar. "What's on my calendar next week?" — "Move the standup to Thursday and invite Ann." — "Decline the 4pm." The agent reads and writes real events on your real calendar, over CalDAV, with no third-party service in the middle.
- 📅 Seven tools, one toolset — list, create, update, RSVP, move between calendars, delete, and enumerate the calendars themselves.
- 🔒 You choose what it may touch — restrict it to specific calendars, and to
specific actions (
read,read,write, …). A disallowed action is not in the toolset at all. - 🛟 Careful with your data — recurrence rules, alarms, and properties this plugin does not model survive every edit, and so does the time zone: a start or end you leave alone is written back as the exact line it was stored as, and one you do change is written in the same zone the event was already anchored to. A weekly standup at 10:00 Berlin stays 10:00 Berlin whether you rename it or move it to 11:00, instead of being flattened to a fixed UTC instant that drifts an hour at the next changeover. An edit that would overwrite someone else's concurrent change is refused; moves copy the resource byte for byte, and the original goes only once the copy has been read back from the target and only if nobody changed it in between.
- 🔑 App password, not your account password — scoped to CalDAV, revocable in one click.
Checked before every release against the latest Hermes release and Hermes main,
each installed by its official installer; Python 3.11–3.14.
Quick start
# 1. Install into Hermes (other ways: see Installing the plugin). It asks for
# your login and an app password, which comes from
# https://id.yandex.ru/security/app-passwords (scope: "Calendar (CalDAV)")
hermes plugins install akinfold/hermes-yandex-calendar/hermes_yandex_calendar --enable
# 2. Skipped the questions, or installed another way? Add the credentials yourself:
(umask 077 && printf 'YANDEX_CALENDAR_LOGIN=%s\nYANDEX_CALENDAR_APP_PASSWORD=%s\n' \
'you@yandex.ru' 'your-app-password' >> ~/.hermes/.env)
chmod 600 ~/.hermes/.env
--enable writes the plugin's manifest name into ~/.hermes/config.yaml for
you, so there is nothing else to switch on:
plugins:
enabled: [yandex_calendar]
Installing from PyPI or the drop-in archive leaves that to you — third-party
plugins are off until they are listed there — and hermes plugins enable yandex_calendar does it.
That's it. Ask the agent "what do I have tomorrow?" and it will tell you.
The backward-compatible default exposes every action, including delete. For a new or untrusted agent setup, start with
YANDEX_CALENDAR_ACTIONS=read— see Security boundaries.
The tools
Up to seven standalone tools, in the yandex_calendar toolset:
| Tool | Purpose |
|---|---|
yandex_calendar_list_calendars |
List the calendars the plugin can use (name + href). |
yandex_calendar_list_events |
List events in a time range (summary, start/end, location, description, attendees, busy status, and an href). |
yandex_calendar_create_event |
Create an event (summary, start, optional end/location/description/all-day, attendees, busy status, target calendar). |
yandex_calendar_update_event |
Edit an event by href: change fields, add/remove attendees, toggle busy/free; an empty string clears a text field. Recurrence rules, alarms, and the time zone of a start or end you leave alone are preserved, and a concurrent change by someone else is refused rather than overwritten. |
yandex_calendar_respond_event |
Respond to a meeting invitation — accept, decline, or tentatively accept. |
yandex_calendar_move_event |
Move an event to another calendar, contents intact. |
yandex_calendar_delete_event |
Delete an event by href. Idempotent: an href that no longer exists, or never did, is also reported as deleted: true. |
Recurring events are not expanded. yandex_calendar_list_events runs a plain
time-range query and reports each event as the server stores it. A recurring series
is listed once, with the start/end of its first occurrence — which can lie well
before the requested range — and its recurrence rule is not part of the output. A
series whose individual occurrences were edited comes back as several entries sharing
one href, and update_event and respond_event refuse such a resource rather than
drop those occurrences; it can still be moved or deleted as a whole.
Yandex Calendar has no public REST API, so this plugin speaks CalDAV
(https://caldav.yandex.ru) directly — the same protocol Yandex's own docs point
third-party clients at. Nothing is proxied through anyone else's servers.
Multiple calendars
yandex_calendar_list_events and yandex_calendar_create_event take an optional
calendar argument; yandex_calendar_move_event takes a required one, naming the
destination. A calendar is named by its display name (as returned by
yandex_calendar_list_calendars), by the last segment of its path (e.g.
events-12345), or by its full href — names and path segments are matched
case-insensitively. Omit the optional argument to use the default calendar.
update, respond, move, and delete identify the event by its event_href,
which already encodes the calendar it lives in.
Restrict which calendars the plugin may touch with YANDEX_CALENDAR_CALENDARS, and
put the one you want as the default first: that order is what
yandex_calendar_list_calendars reports and what an unqualified write follows.
Without the setting, the order the server returns stands.
Changed in 0.5.0. Until then the order of that setting was ignored and the default was whichever allowed calendar the server happened to list first. If you set
YANDEX_CALENDAR_CALENDARSand relied on that, check which calendar your value names first before upgrading — it is the one events will go to now. An entry that matches nothing is skipped, so a typo in first position hands the default to the next entry that does match.
Some Yandex calendars — holidays and birthdays, for instance — are read-only, yet they
are listed like any other and the server answers a write into them with success while
storing nothing. yandex_calendar_move_event catches this, because it reads the copy
back and leaves the original where it was, but yandex_calendar_create_event does not:
it reports created: true and an href at which nothing exists. Keep such calendars
out of reach with YANDEX_CALENDAR_CALENDARS.
Configuration
| Env var | Required | Default | Meaning |
|---|---|---|---|
YANDEX_CALENDAR_LOGIN |
yes | — | Yandex login / email. |
YANDEX_CALENDAR_APP_PASSWORD |
yes | — | App password for CalDAV — an account password will not work. |
YANDEX_CALENDAR_BASE_URL |
no | https://caldav.yandex.ru |
HTTPS override for self-hosted / testing. |
YANDEX_CALENDAR_CALENDARS |
no | (all) | Comma-separated allow-list of calendars, e.g. Work,Personal. The order matters: the first entry that matches a calendar makes it the default, the one an unqualified create or list uses, and the listing follows the same order. Each entry is a calendar name or the last segment of its href (e.g. events-12345), matched case-insensitively; a full href is not accepted here. An entry that matches nothing is ignored and claims no position, and if none match, yandex_calendar_list_calendars returns an empty list while every other tool fails with No calendars available matching …. |
YANDEX_CALENDAR_ACTIONS |
no | (all) | Comma-separated allow-list of actions the agent may perform — see below. |
Credentials are read from the environment first, then from ~/.hermes/.env, so they
work in gateway and subprocess runs. Secret values are never logged.
Dates and times are ISO 8601 (2026-07-25 or 2026-07-25T14:00:00+03:00); a datetime
without an offset is treated as UTC.
Restricting what the agent can do
YANDEX_CALENDAR_ACTIONS decides which of the seven tools are registered at all.
A disallowed action is not merely refused at call time: the tool never appears in
the agent's toolset, so it cannot be invoked, and the model is not tempted to try.
Accepted values, comma-separated and case-insensitive — individual actions
(list_calendars, list_events, create_event, update_event, respond_event,
move_event, delete_event), full tool names (yandex_calendar_delete_event), or
the shorthands:
| Shorthand | Expands to |
|---|---|
read |
list_calendars, list_events |
write |
create_event, update_event, respond_event, move_event |
delete |
delete_event |
all |
everything (the default) |
# Look, but don't touch:
YANDEX_CALENDAR_ACTIONS=read
# Full scheduling, but the agent can never delete anything:
YANDEX_CALENDAR_ACTIONS=read,write
# Just enough to answer invitations:
YANDEX_CALENDAR_ACTIONS=list_events,respond_event
Leave it unset for all seven tools; an empty or whitespace-only value counts as
unset, so YANDEX_CALENDAR_ACTIONS= does not mean "no tools". A name that
matches nothing is ignored, so a typo can only ever withhold a tool, never grant one
— and a non-blank value that names nothing recognisable therefore registers nothing
at all. The list is applied when the plugin loads: restart Hermes after changing it.
Security boundaries
- CalDAV credentials are sent only to the HTTPS origin configured by
YANDEX_CALENDAR_BASE_URL. Event hrefs pointing to HTTP or another origin are rejected before a request is made. Cross-origin redirects do not receive the authorization header. YANDEX_CALENDAR_CALENDARSapplies to both target calendars and direct event operations (update,respond,move, anddelete). Event paths are canonicalized before the allow-list check and the same canonical path is sent; moves validate both the source and destination calendar.- Calendar titles, descriptions, locations, and attendee names are untrusted input.
A capable agent can still act on misleading event content, so use
readunless the agent and every calendar writer are trusted. Attendees supplied to a tool must be mailbox addresses, while server-provided calendar addresses are preserved for compatible invitation updates. Line breaks are rejected during serialization to prevent CRLF property injection. - Edits are conditional. The plugin writes an event back only if it still matches
the version it read (
If-Matchwith the resource'sETag), so an agent cannot silently overwrite a change you made meanwhile in the Yandex web UI, on a phone, or from another client. When that happens the tool says so and the agent can re-read the event and reapply its change. A move is conditional at the same point: the original is removed only if it still matches the copy that was just written, and if it changed in between the event is left in both calendars with a message saying so, rather than losing that change. - The action allow-list limits the tools exposed to Hermes; it does not reduce the privileges of the Yandex app password itself. Use a dedicated app password and, for stronger isolation, a dedicated Yandex account.
The default remains all for compatibility with existing installations. This is a
trusted-agent mode, not the recommended starting point for a new deployment.
Getting the app password
CalDAV does not accept your normal account password.
- Open https://id.yandex.ru/security/app-passwords.
- Add a password with the Calendar (CalDAV) scope.
- Copy it into
YANDEX_CALENDAR_APP_PASSWORD— it is shown only once, and you can revoke it at any time without touching your account password.
If a tool answers "Authentication failed", this is almost always the cause.
Installing the plugin into Hermes
Option A — from Git (recommended)
hermes plugins install akinfold/hermes-yandex-calendar/hermes_yandex_calendar --enable
Note the /hermes_yandex_calendar at the end. The plugin lives in that directory,
not at the repository root, and Hermes reads the manifest from whatever you point
it at. Name the directory and the install is a plugin: Hermes prompts for
YANDEX_CALENDAR_LOGIN and YANDEX_CALENDAR_APP_PASSWORD, installs under the
manifest name yandex_calendar, and --enable enables that name. It also scans
only that directory, so the tests and workflows in this repository stay out of the
security report.
Point it at the repository root instead and Hermes copies a directory with no
manifest and no register(ctx) in it: it warns that it "may not be a valid Hermes
plugin", asks for no credentials, and does not enable the plugin. What gives it
away is hermes plugins list, which still shows yandex_calendar — the package
nested in the clone is found — but with not enabled beside it. If you are in
that state, remove ~/.hermes/plugins/hermes-yandex-calendar and install again
with the directory named.
Option B — from PyPI
For a Hermes whose Python environment you manage yourself — your own virtualenv,
a Nix build: install hermes-yandex-calendar into that environment, then enable
the plugin. Hermes finds it through the hermes_agent.plugins entry point, and
httpx and defusedxml come along as its dependencies.
hermes plugins enable yandex_calendar
Not for a standard Hermes install. Since 24 September 2026 the official installer runs Hermes from environments its package manager builds and replaces, and Hermes does not support adding packages to them by hand: use Option A or C.
A Hermes in the older layout — one that runs from ~/.hermes/hermes-agent/venv
and keeps its own uv in ~/.hermes/bin, with no ~/.hermes/installs, which is
what Hermes 0.21.5 and anything installed before that date and not updated since
look like — takes the package like this:
~/.hermes/bin/uv pip install --python ~/.hermes/hermes-agent/venv/bin/python hermes-yandex-calendar
hermes plugins enable yandex_calendar
Switch such an install to Option A before you run hermes update: the update
moves Hermes onto the new environments, and a package installed this way does not
come along. A bare pip install hermes-yandex-calendar never reached Hermes at
all — the pip on your PATH belongs to some other Python.
Nothing asks for credentials on this path — add them to ~/.hermes/.env as in
the Quick start.
Option C — drop-in directory
Download hermes-yandex-calendar-plugin-<version>.zip from the
latest release
— not the wheel, the .tar.gz, or GitHub's "Source code" archives — then unzip it
into ~/.hermes/plugins/ and enable it:
unzip hermes-yandex-calendar-plugin-<version>.zip -d ~/.hermes/plugins/
hermes plugins enable yandex_calendar
You should end up with ~/.hermes/plugins/yandex_calendar/plugin.yaml. Nothing
asks for credentials here either: add them to ~/.hermes/.env yourself.
Dependencies on Options A and C
Options A and C copy the plugin's sources and install nothing else, so httpx and
defusedxml have to be in the environment Hermes runs in already. A standard
Hermes install has both — checked on Hermes 0.21.5 and on main, and again by
the install check before every release — so there is nothing to add.
If you built Hermes' environment yourself and it lacks one, the plugin fails to
load with No module named 'defusedxml' (or 'httpx'). Install the missing
package into that environment the way you manage it, or install the plugin by
Option B, which brings both with it.
Upgrading
Upgrade the way you installed. Option A — Hermes 0.21.5 and later install the new version from the source they recorded:
hermes plugins update yandex_calendar
Older Hermes cannot update an install from a plugin directory, and no Hermes
updates one pinned with --ref: run the install command again with --force,
which replaces the installed copy and keeps your credentials.
hermes plugins install akinfold/hermes-yandex-calendar/hermes_yandex_calendar --enable --force
Both scan the new version again, but unlike a first install neither stops to ask when Hermes' security scan reports a caution.
Option C — unzip the new release's archive over the old one:
unzip -o hermes-yandex-calendar-plugin-<version>.zip -d ~/.hermes/plugins/
Option B — upgrade the package in the same environment: in the older layout,
~/.hermes/bin/uv pip install --upgrade --python ~/.hermes/hermes-agent/venv/bin/python hermes-yandex-calendar
Options A and C, and their upgrades, are checked before every release by
installing the build into a real Hermes — the latest release and main, each set
up by its official installer — exactly as written here; the Option B install is
checked on the latest release while it still has the older layout. See
Checking the install paths.
Development
python -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'
ruff check . && ruff format --check .
pytest # unit tests, no network
Running the live E2E tests
The e2e-marked tests hit a real Yandex account and are deselected by default.
They create, edit, move, and then delete throwaway events 400–402 days out, named
with the hermes-e2e prefix (YC_E2E_MARKER overrides it). Most land in the default
calendar; the move test also drops a short-lived probe event into your other
calendars, one at a time, until it finds one that accepts it, and then moves an event
there. Everything is deleted again, so a successful run leaves nothing behind — but
the attendees they invite do receive an invitation, so use addresses you own.
Locally
YANDEX_CALENDAR_LOGIN=you@yandex.ru \
YANDEX_CALENDAR_APP_PASSWORD=xxxx \
YC_E2E_ATTENDEES=you+guest@yandex.ru \
pytest -m e2e
YC_E2E_ATTENDEES is the comma-separated list of addresses the throwaway event
invites, and each one really is emailed an invitation — so list mailboxes you own.
Omit it and the suite falls back to a +e2e sub-address of the account itself,
which lands in your own inbox. That fallback needs YANDEX_CALENDAR_LOGIN in full
e-mail form (you@yandex.ru); with a bare login there is nothing to derive it from
and the round-trip test fails asking for YC_E2E_ATTENDEES. Two constraints, both learned the hard way against
the live server:
- The addresses must exist. A made-up one (
guest@example.com) bounces back into your mailbox. - They must not resolve to the account itself. Yandex canonicalises addresses
(
@ya.ru→@yandex.ru) and drops an attendee that equals theORGANIZER, so a plain alias of your own login silently disappears and the round-trip check fails. A+tagsub-address is delivered to the same mailbox but stays a distinct attendee.
Or keep all three out of the command line, in ~/.yandex-calendar-login,
~/.yandex-calendar-app-password, and ~/.yandex-calendar-attendees, and just run
pytest -m e2e — see tests/e2e/conftest.py.
The suite builds its client exactly as the plugin does, so every YANDEX_CALENDAR_*
setting it does not find in the environment (or, for the credentials, in those files)
is looked up in ~/.hermes/.env — YANDEX_CALENDAR_BASE_URL and
YANDEX_CALENDAR_CALENDARS included. On a machine with Hermes already configured,
pytest -m e2e therefore runs against that account instead of skipping.
On GitHub Actions
The E2E (live) workflow is manual (workflow_dispatch). It reads
YANDEX_CALENDAR_LOGIN, YANDEX_CALENDAR_APP_PASSWORD, and YC_E2E_ATTENDEES
from the secrets of a GitHub Environment named yandex-calendar-e2e. All three
must be secrets — the workflow reads nothing from environment variables, so a value
defined as a variable arrives empty: without the credentials every test is skipped,
and without YC_E2E_ATTENDEES the suite falls back to the +e2e sub-address. It
runs the plugin inside a real Hermes, installed by its official installer: the
latest Hermes release by default, and its hermes input switches to Hermes main
or to no Hermes at all. With Hermes, the tests run in Hermes' own Python,
with this checkout on its path rather than installed into Hermes' environment, and
the run fails outright if the plugin cannot import Hermes, rather than testing the
plugin's stand-ins instead.
Checking the install paths
The install-marked tests in tests/install/ install the built plugin into a
real Hermes, set up by its official installer, by each option in
Installing the plugin into Hermes — running
the README's own commands, upgrades included — and then ask Hermes what it
loaded: the plugin must be listed as enabled, load without error, and give the
agent its tools. For Options A and C they also check that Hermes' own Python
already has httpx and defusedxml, and they check that installing from the
repository root still looks the way this README describes. A fast unit test keeps
the commands in the tests and in this README identical.
The Install check workflow runs them against the latest Hermes release and
against Hermes main on every pull request, and on every release tag before
anything is published: the GitHub Release and the PyPI upload both wait for it.
They install into a real ~/.hermes, so run them yourself only in a container or
VM — see the docstring of tests/install/test_install.py.
Related Hermes plugins
Part of a family of Yandex plugins for Hermes Agent:
- hermes-yandex-disk — browse, read, write, and share files on Yandex Disk (REST API).
- hermes-yandex-mail — search, read, flag, move, and delete Yandex Mail messages over IMAP, and send over SMTP when sending is switched on.
- hermes-yandex-search-api — Yandex web search backend and generative, cited answers for Hermes (Yandex Search API).
Contributing
Issues and PRs are welcome — see CONTRIBUTING.md for the layout, the plugin contract rules worth knowing, and the release process.
License
MIT — see LICENSE.
Metadata
Release files for hermes-yandex-calendar 0.6.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hermes_yandex_calendar-0.6.3.tar.gz | 70.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hermes_yandex_calendar-0.6.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 110.7 kB
Release files / hermes_yandex_calendar-0.6.3.tar.gz
| Download URL | hermes_yandex_calendar-0.6.3.tar.gz |
|---|---|
| Size | 70.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4f5bddbbf279bd3a4d3473b48c6f9512b5312be4e2675797ccfeed6838076b71
|
|
BLAKE2b-256 checksum How to use checksums |
858ca7f06c916550ca35fb4854175399eebe55202c8e86297187094513cbc283
|
| 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 Sep 26, 2026.
Transparency logRelease files / hermes_yandex_calendar-0.6.3-py3-none-any.whl
| Download URL | hermes_yandex_calendar-0.6.3-py3-none-any.whl |
|---|---|
| Size | 40.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7eb135f39f88ddf809ed26395fb582321ea44702894177eb3bab461f5abfb8e2
|
|
BLAKE2b-256 checksum How to use checksums |
3ae89f90e82c29fd8e95ce1c5a7bc049e09dfa6c3e85d1225c051122f7b92e1d
|
| 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 Sep 26, 2026.
Transparency log