author-feed
Read one public profile and that author's recent posts, and watch one saved account, built on the public AppView at public.api.bsky.app.
The plugin sends HTTPS GET only to https://public.api.bsky.app, with no key and no Authorization header. The only paths are /xrpc/app.bsky.actor.getProfile and /xrpc/app.bsky.feed.getAuthorFeed. It does not call search, a firehose, a PDS, api.bsky.app, bsky.social, or an image host. There is no POST, PUT, or DELETE. hermes author-feed schedule writes a script, and Hermes starts that Python when the cron job fires. Redirects (301, 302, 303, 307, 308) are not followed. Each GET stops if 20 seconds pass with no data, and a single GET is also cut off after 30 seconds in total, counted from before the connection opens, so a slow header or a slow body cannot hold that GET open longer. One watch run reads at most 3 pages, so at most three such GETs, and therefore at most 90 seconds. It is not retried in the same run. On profile_get and author_feed the timeout text is: Not fetched. Saved state is unchanged. 取れなかった。保存は変わっていない。 Those calls do not write the state file. On author_watch the same failure leaves seen URIs unchanged, records the failure, and says so. It does not say the request may have taken effect.
profile_get requires actor and returns did, handle, displayName, followersCount, followsCount, and postsCount. It drops avatar, banner, description, and pinnedPost. author_feed requires actor and requests one page with limit=10 and filter=posts_no_replies. It does not send includePins, a caller-chosen filter, or a caller-chosen limit, and it does not sort the page again. author_watch takes no arguments. Unknown arguments are rejected before any request, as is an actor that is empty, holds whitespace, or is longer than 253 characters. The two read tools do not write seen post URIs. A manual read does not advance them.
Replies are not read: the feed is requested with filter=posts_no_replies, so a reply, including the account's own thread continuation, is not listed by author_feed and is not announced by the watch.
A repost is kind=repost and the text is the original author's, with author_handle. It is not announced as the watched account's post. A pin is not treated as new and its URI is not stored. On 2026-10-08, posts_no_replies for bsky.app still included reposts by aster.id and kenjennings.bsky.social. includePins=true with limit=3 returned an older pinned post. This plugin does not send that parameter. An empty feed array is an empty page, not a failure, and does not clear seen URIs. postsCount is not the cursor. A missing actor was HTTP 400 InvalidRequest / Profile not found, not an empty success. HTTP 200 with an empty body, non-JSON, an error field, or no feed array is a failure. BlockedActor and BlockedByActor were not seen live. If that error name is returned, the call fails.
Empty post text with an embed returns embed_type as the embed $type only. Text longer than 300 characters is cut and text_truncated is true. At most 20 label val strings are returned, each cut at 100 characters. Images and blobs are not downloaded. The request asks for 10 items. A page can be shorter than 10. On 2026-10-09, getAuthorFeed for actor=bsky.app with limit=10 and filter=posts_no_replies returned HTTP 200, a page shorter than 10, and a cursor. The plugin does not say the whole history was seen. author_feed is one GET and does not follow that cursor.
hermes author-feed schedule saves one account after getProfile succeeds. A failed schedule leaves the old DID, seen URIs, and job in place. The same DID keeps seen URIs and updates the handle string. A different DID clears seen URIs and sets the baseline flag false. The reply names both accounts, how many saved URIs were cleared, and that the next run stores a new baseline without announcing it. There is one cron job named author-feed, stored by id. Updates go to that id. The plugin does not remove a job by name. hermes author-feed unschedule removes the job and the watch script, and leaves the state file, including seen URIs. Run unschedule before hermes plugins remove. After the plugin is removed, that CLI is gone, so remove the leftover job with hermes cron remove <id>, and add -p <name> when the profile is not default. That command leaves scripts/author-feed-watch.py in the Hermes home, so delete that file too. If the saved id is already gone, unschedule clears it and says the job is already gone. hermes author-feed status reads the file, checks whether the saved id still exists, and does not fetch. If that id is gone, or no id is stored, status says so and prints no hermes cron run or hermes cron remove line, because both need an id.
The default interval is every hour. Omitting --every on a later schedule keeps the saved interval. The first schedule with no saved interval is every hour. Intervals faster than every 15 minutes, intervals that are not a multiple of 15 minutes, intervals longer than every 7 days, and a cron expression you type are rejected. The words you see are every hour, every 15 minutes (or every N minutes for another allowed multiple, such as every 45 minutes), or every N hours for a whole number of hours, so a mistyped interval is not printed as six million minutes. author_watch reads at most 3 pages when none of the saved posts or stored repost marks is on the current page. It does not retry a failed GET in the same run. A PDS request limit is not cited as an AppView limit. No fee is stated for this endpoint. There is no daily cap.
Delivery accepts origin, local, all, bot-chat, bot-chat:<profile>, the Hermes 0.21.6 cron platform names (telegram, discord, slack, whatsapp, signal, matrix, mattermost, dingtalk, feishu, wecom, wecom_callback, weixin, sms, email, webhook, bluebubbles, qqbot, yuanbao), a platform:<chat id> on those names, and platform names from the public Hermes platform registry that declare a cron delivery env var. The registry is not a list of what you have set up. Nothing here checks that the platform is configured. On Hermes 0.21.6 and current main, a job whose target is not set up does not run: Hermes records it as blocked_config, which hermes cron runs <id> shows, and hermes cron doctor reports it as last run failed. A name this plugin refuses can still be stored with hermes cron edit <id> --deliver <target>. For a platform target, schedule prints that caution with the platform name. The default deliver target is local, and local does not send to chat. cli, cron, api_server, and words that look like a schedule are rejected, and the existing job stays. Replacing a target echoes the previous one. bot-chat puts each report, including the watched account's post text, into another model turn that has the profile's tools. It is not model-free.
Each watch run costs 0 tokens, and the unmanned run holds no tools. hermes author-feed schedule writes author-feed-watch.py under HERMES_HOME/scripts and creates the job with hermes cron create --no-agent --script. When HERMES_HOME is unset, that directory is the Hermes default HOME/.hermes/scripts, from hermes_constants.get_hermes_home(), and the child hermes cron is started with that path set as HERMES_HOME. Hermes runs that script with its own Python and delivers stdout as the message. Empty stdout sends nothing. A non-zero exit is a failed run. interpreter is not passed. Running schedule again updates the interval and deliver target of the existing job and switches it with hermes cron edit --script --no-agent. A job made by 0.1.x keeps its model turn until that next schedule. hermes cron create has no per-job tool list on Hermes 0.21.6 or 0.22.2, which does not matter for this job because the unmanned run holds no tools. The watched account's post text is the script output. bot-chat is still another model turn.
Seen URIs advance when the fetch succeeds and this process is the cron job that schedule saved. The author_watch tool reads the public cron.execution_identity.current_cron_execution, and its job_id must equal the saved id. The script does not read that helper. The helper lives in the scheduler process, and the script is a separate process. The script reads cron/executions.db for rows with status running whose pid equals the parent of this process, then reads cron/jobs.json and keeps a job only when the script file name is author-feed-watch.py. It advances seen URIs only when that set is exactly the saved id. It does not import the cron job store. A direct run of the script and another cron job send no GET and write nothing. The script exits 3 and writes the reason to stderr, so the run is a failure. An empty stdout is not used for that refusal, because empty stdout looks like no new posts. When another cron job calls author_watch, it sends no GET and writes nothing. If the saved id is already gone, error_type is job_gone. If the job list cannot be read, error_type is cron_unread. If the saved id is still listed and this run's job_id differs, error_type is other_cron_job. The same other_cron_job holds when no id is saved. When the plugin cannot tell that this run is a cron job, including a separate plugin host process, it says it cannot monitor, names plugins.isolation: in_process as the setting to check, sends no GET, and leaves saved URIs unchanged. It does not say there is nothing new. It posts that notice on every run. A manual call does not quiet the next cron. An environment variable named HERMES_CRON_SESSION alone does not count as that proof. The first successful cron run stores up to 3 pages and does not announce them. A page with no post by the account still stores the baseline. If those 3 pages were read and the feed still offered another cursor, the baseline message says so. A post found on page 2 or later may be older than that first stored page. On the baseline, pages with no post by this account say so. Repost URIs are not stored as this account's posts. Each repost row that was read is stored as a continuation mark: the reason's indexedAt and the original post's URI. One run reads at most the latest 3 pages and announces a new post by this account on them. When every row on those pages is new, with no saved post or mark, and the feed still offers another cursor, it also posts one possible-miss notice, even when no post by the account was on those pages. The rows it read are stored. When those rows are still on the pages the next run reads, that run overlaps and does not repeat the notice. When newer rows push them past the pages it reads, the same notice is quiet for 24 hours, and a new post by this account on the pages it read is still announced. A run that finds a saved post or a stored mark does not post that notice. That includes a run where nothing changed, and a run where new rows only pushed a stored mark deeper while the mark was still on the pages it read. A new post by the account that sits above a stored mark is announced. A new repost of an already seen post is a different row, so it does not stop the walk. A page shorter than 10 does not stop it either, because getAuthorFeed drops filtered rows after the limit is applied. Only a missing cursor ends it. If instead the feed ran out of cursors, the whole feed was read, so nothing slipped past and that note is not sent: an account whose posts were deleted, or an empty feed, stays quiet. It does not warn on every later run. That note is sent on the first such run, again when the gap closes and later reopens, and otherwise at most once every 24 hours. The runs in between do not repeat that note, and they still announce a new post by this account that they read. Seen URIs are not cleared. A full page that still holds a saved post does not add that note, and the plugin does not append "Older posts were not fetched." Saved URIs and continuation marks are each capped at 2000. Past the cap the entries stored longest ago are dropped first, and the lookup is a set.
The same upstream failure notifies on the first time, when the cause changes, and again after 24 hours. The cause is counted per HTTP status, so a change from 503 to 400 notifies. The same status within 24 hours stays quiet. Recovery notifies once. Argument errors and a missing saved account are not counted as upstream failures. notify is present on success and failure. Every failed tool result has ok false and an error_type: the upstream cause (http_<status>, timeout, network, redirect, oversize, bad_body, bad_path, or another refused request), or bad_args, not_cron, other_cron_job, job_gone, cron_unread, no_saved_account, state_corrupt, state_unreadable, or save_failed. A successful run with nothing new has notify false and the message is "No new posts." A quiet repeat of a possible miss says the same possible miss was already reported only when that run has no new post by this account to announce. When it did read one, it announces that post and does not add that sentence. A corrupt state file is not overwritten and that run does not change seen URIs.
State stays under <HERMES_HOME>/plugin-data/author-feed, per profile. If that folder cannot be read, the plugin refuses. It does not fall back to another home path. Writes go through the Hermes write check and replace a temporary file. The file stores the DID, handle, deliver target, interval words, job id, baseline flag, your account's post URIs, continuation marks for repost rows that were read (the reason's indexedAt and the original post URI), the last failure code and its times, when the possible-miss note was last sent, and the last success time. Saved URIs and continuation marks are each capped at 2000. It does not store post text, chat speaker names, speaker ids, a key, postsCount, or the API cursor. The state file is not sent as a MEDIA: path. That folder survives hermes plugins remove: the watched DID, handle, deliver target, and up to 2000 post URIs stay on disk. To delete them, remove the folder yourself with rm -r <HERMES_HOME>/plugin-data/author-feed, using the HERMES_HOME of the profile you installed under.
The watch still reads one unpublished Hermes part, the write check, and a Hermes update can stop the watch. Schedule, unschedule, and status use the public hermes cron command and do not import the cron job store. The cron proof for the author_watch tool reads the public cron.execution_identity.current_cron_execution. The scheduled script calls the public hermes_constants.get_hermes_home to find plugins/author-feed. Schedule, status, and unschedule call that same function for the directory they share with hermes cron. When HERMES_HOME is unset, the directory is the Hermes default, HOME/.hermes on this platform. The child hermes cron is started with that path set as HERMES_HOME, so the script and the job are written in one directory. The script also reads cron/executions.db and cron/jobs.json under that home. It does not import the cron job store. A Hermes update that changes those files can stop the watch. The author_watch tool advances seen URIs only when that helper returns a run whose job_id is the saved id and HERMES_PLUGIN_HOST_PROCESS is unset, empty, or 0. A missing module, a renamed helper, a raised read, or a run without a job id is not a cron run. The environment variable HERMES_CRON_SESSION is not read. get_session_env is not called. Besides the fixed Hermes 0.21.6 names, the delivery allowlist reads the public gateway.platform_registry. If that registry cannot be imported, the fixed names are still accepted. The profile name is ctx.profile_name, read once in register. This plugin does not import hermes_cli.profiles, does not read gateway.session_context, and does not stat auth.json or .env. The no-agent child inherits the Hermes process environment. Hermes can place values from .env into that environment, so a child process can read them. The watch script author-feed-watch.py does not open .env. Writes go through agent.file_safety.get_write_denied_error.
The read tools have no user list: anyone you allow on your gateway can call them, and hermes tools turns them off for each surface. There is no slash command. User-visible text is English first. requires_hermes is >=0.21.6, the first release with current_cron_execution. On 0.21.4 and 0.21.5 the plugin does not load. A job left from that install keeps its model turn, so remove it with hermes cron remove <id>. A job made by 0.1.x on a newer Hermes keeps its model turn until the next schedule. manifest_version is 2. python_dependencies is empty. The license is MIT. This repository does not include a NOTICE file. pre_tool_call is not registered. The unmanned run holds no tools, so there is nothing to narrow. A conversation cannot advance seen URIs, because author_watch refuses it. A direct run of the watch script and another cron job cannot advance them either, because the script exits 3 unless the running execution is the saved job. This plugin does not call Hermes hooks itself. Schedule does not ask for an approval prompt. Changing the watched account is the local CLI only.
While this plugin is enabled, its bluesky-posts skill adds one line to the skills index of every session on every platform. That line stays on a surface where hermes tools turned the tools off. The skill can still be read there, but the tools are unavailable. The model in any session, including one a gateway admits a guest to, can load the skill. The skill is instructions only and sends nothing. hermes plugins disable author-feed removes the tools and the skill together. It does not remove a job made with schedule. Run hermes author-feed unschedule first, or later hermes cron remove <id>.
Printed hermes cron lines come after the heading "Hermes commands for this job, for you to run:", so they read as commands for you to run. They are not a record of commands already run. hermes cron run and hermes cron remove include the job id and are left out when there is no id. create, list, and status do not take an id and are always printed. When the current profile is not default, every printed line includes -p <name>. The name is ctx.profile_name, captured when the plugin registers. If that value is missing, the lines omit -p, the same as the default profile. This CLI does not define -p or --profile of its own.
On 2026-10-08, origin/main 38880bd2f1e90dbc9a1aeec03af62539ee64719a had no catalog tool that calls this AppView. The one grep hit was an achievements README image size. RSS tools are a different reader. Open pull-request titles were not all read, and gh was not used.
What was tried
The design table below is 13 GETs on 2026-10-08 to https://public.api.bsky.app, 1.1 seconds apart, with no key and no redirect. Those calls were not a plugin install. No RateLimit header, Retry-After, or Location was returned.
| Call | Result |
|---|---|
| getProfile actor=bsky.app | HTTP 200 with did, handle, displayName, followersCount, followsCount, postsCount |
| getAuthorFeed actor=bsky.app&limit=5 and no filter | HTTP 200, five items, including two reposts |
| getAuthorFeed filter=posts_no_replies&limit=5 | HTTP 200, and reposts were still in the page |
| getAuthorFeed filter=posts_and_author_threads&limit=5 | HTTP 200, same five items |
| getAuthorFeed includePins=true&limit=3 | HTTP 200, four items, first was a pin from 2024-10-17 |
| getProfile missing actor | HTTP 400 Profile not found |
| getAuthorFeed same missing actor | HTTP 400 Profile not found |
| getAuthorFeed actor=not a handle | HTTP 400 |
| getAuthorFeed limit=0 and limit=101 | HTTP 400 |
| getAuthorFeed filter=nope | HTTP 200, ordinary feed |
| searchPosts q=hello | HTTP 403 HTML. This plugin does not call it |
| getAuthorFeed by the returned DID | HTTP 200 |
Not verified
- The 13 GETs in the table above were not a plugin install and did not run cron or a gateway.
BlockedActorandBlockedByActorwere not seen live. If that error name is returned, the call fails.- No RateLimit header and no 429 were seen. A numeric AppView cap was not taken from the documentation.
- Open pull-request titles were not all read.
- Authenticated endpoints, writes, a firehose, and image hosts were not called.
posts_with_mediaandposts_with_videowere not sent.- The terms were not settled here as a list of what a repeated fetch is allowed or forbidden to do.
- The feed in that table is the 2026-10-08 page of the public account bsky.app. The 2026-10-09 check was one
limit=10page, not a full history.
Terms and marks used for the name: Bluesky Terms of Service, Bluesky Trademark Policy, AT Protocol Trademark Policy, and the public AppView note on rate limits. The product name does not include those marks. The host name and lexicon method names above are the published identifiers.
README from the reviewed commit d3c9497; it updates when the author re-pins.

TakeshiTGAL
