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, and it starts no program on your machine. 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 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. 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 uses an allowlist: origin, local, all, bot-chat, bot-chat:<profile>, the names on the Hermes list of delivery platforms, and names added by loaded plugins. That Hermes list is a fixed set of platform names; it is not a list of what you have set up. Nothing here checks that the platform is configured, and neither does hermes cron doctor, so an accepted name whose platform is not set up silently drops the report. For any target other than origin, local, and all, schedule prints that caution with the name. The default deliver target is local, and local does not send to chat. If the Hermes list cannot be read, only origin, local, and all are accepted. 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 spends another model turn on the fetched text. It is not model-free.
The cron prompt tells the model to call author_watch with an empty argument object only. If notify is true, reply with message only. If notify is false, reply [SILENT]. If the tool is missing, say the watch is not running. The prompt does not ask the model to search or to complete post text. A model may add words, and that cannot be stopped. The job is a model turn. A tool-calling run can take two or more requests to your model. interpreter is not passed, and the job is not no_agent.
Seen URIs advance only when author_watch is confirmed as the cron job and the fetch succeeds. When the plugin cannot tell that this run is the 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 rather than warning forever. 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. 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 reads unpublished Hermes parts, and a Hermes update can stop the watch. The cron proof reads gateway.session_context for a contextvars.ContextVar whose name is HERMES_CRON_SESSION. Hermes stores that variable as _CRON_SESSION, with sentinel _UNSET. Seen URIs advance only when its value is exactly 1 and HERMES_PLUGIN_HOST_PROCESS is unset, empty, or 0. get_session_env is not called. The delivery allowlist reads cron.scheduler_delivery._KNOWN_DELIVERY_PLATFORMS and gateway.platform_registry. The profile name reads hermes_cli.profiles.get_active_profile_name and _get_profiles_root, and hermes_constants.get_hermes_home.
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.4. manifest_version is 2. python_dependencies is empty. The license is MIT. This repository does not include a NOTICE file. No hooks are registered, and Hermes hooks are not called again. Schedule does not ask for an approval prompt. Changing the watched account is the local CLI only.
Printed hermes cron lines come after the heading "Hermes commands for this job, for you to run:", so they read as commands to run rather than as 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>. 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, no redirect, and User-Agent takeshitgal-design-probe. 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.