Standalone Hermes Agent platform plugin for Linear Agent Sessions. It lets a Hermes gateway profile appear in Linear as an app-attributed Linear Agent, receives signed Agent Session webhooks, sends responses as Agent Activities, and provides 58 linear_agent_* tools whose mutations are attributed to the app.
This is an independent third-party plugin: it does not patch or vendor the Hermes core repository.
From Git (recommended):
hermes plugins install infinityrobot/hermes-linear-agent --enableOr, after a package release, install it into the same Python environment as Hermes:
python -m pip install hermes-linear-agent
hermes plugins enable linear-agentHermes itself is not distributed as a wheel and must not be added as a pip dependency. The Git installation path above is recommended for normal Hermes installations; the wheel path is intended for managed or development environments that already contain Hermes.
Then verify discovery and run the platform setup wizard:
hermes plugins list
hermes gateway setupThe plugin ID is linear-agent; the gateway platform and toolset key remain linear_agent.
For Linear Agent/app attribution, use Linear OAuth 2.0 client credentials. Hermes stores the Linear OAuth app client ID/secret in the active profile .env. This plugin mints an app-actor access token with grant_type=client_credentials, caches it in profile-scoped plugin storage at plugin-state/linear-agent/oauth.json, and gets a new token before expiry or after a Linear GraphQL 401. No browser flow or refresh token is required.
Fastest path: hermes gateway setup → Linear Agent — the wizard prompts for credentials, verifies them against Linear, auto-detects the app user ID, and builds the allowlist interactively. Manual steps below.
-
In Linear, create or open the OAuth application for the agent and enable/use client credentials for the app.
-
Add the OAuth app credentials to the profile
.env:LINEAR_AGENT_CLIENT_ID=... LINEAR_AGENT_CLIENT_SECRET=... LINEAR_AGENT_OAUTH_SCOPES=read,write
-
Optional one-time mint/test for a pip or source installation:
hermes-linear-agent-oauth \ --profile <profile> \ --client-credentials
The helper reads
LINEAR_AGENT_CLIENT_IDandLINEAR_AGENT_CLIENT_SECRETfrom the profile.env, writes the cached access token/expiry to plugin-owned state, and does not print token values. Runtime token reissue works from the client ID/secret even if the cached token is absent. Git-only installations can usehermes gateway setupinstead. -
Restart the profile gateway:
hermes gateway restart
At runtime, the adapter uses client credentials first, reads and persists the current access token/expiry in plugin-owned profile state by default, and retries once after a Linear GraphQL 401. Set LINEAR_AGENT_PERSIST_TOKENS=false to disable access token persistence. For migration, the plugin can read an older providers.linear_agent entry from Hermes' shared auth.json, but never writes to that file.
The older authorization-code + localhost callback flow is still available as a fallback when LINEAR_AGENT_REFRESH_TOKEN is configured. Run the same helper without --client-credentials to exchange a browser authorization code and cache rotating access/refresh tokens in plugin-owned state.
If you already have a valid Linear app/OAuth token, you can still set secrets manually in the profile .env:
LINEAR_AGENT_ACCESS_TOKEN=lin_oauth_or_app_token
LINEAR_AGENT_WEBHOOK_SECRET=linear_webhook_signing_secretOptional:
LINEAR_AGENT_CLIENT_ID=
LINEAR_AGENT_CLIENT_SECRET=
LINEAR_AGENT_REFRESH_TOKEN=
LINEAR_AGENT_TOKEN_EXPIRES_AT=
LINEAR_AGENT_REDIRECT_URI=
LINEAR_AGENT_OAUTH_SCOPES=read,write
LINEAR_AGENT_OAUTH_ACTOR=app
LINEAR_AGENT_APP_USER_ID=
LINEAR_AGENT_WORKSPACE_ID=
LINEAR_AGENT_HOME_TARGET=
LINEAR_AGENT_ALLOWED_USERS=
LINEAR_AGENT_ALLOW_ALL_USERS=
LINEAR_AGENT_AUTH_PATH=Authorization is two-layer and fail-closed at both layers: the gateway grants access via LINEAR_AGENT_ALLOWED_USERS / LINEAR_AGENT_ALLOW_ALL_USERS (no allowlist means every user is denied), and the adapter checks the sender BEFORE any webhook side effect by consulting the gateway-registered authorization chain (the same one dispatch uses — env allowlists including the * wildcard, GATEWAY_ALLOWED_USERS, DM-pairing grants), with YAML allowed_users / allow_all_users granting as a union. With nothing configured anywhere, webhooks are rejected 403 and no ack/auto-start/stop activity ever fires. allowed_teams narrows further and never grants.
LINEAR_AGENT_WEBHOOK_SECRET is effectively required: unsigned webhooks are rejected unless you explicitly opt out with allow_unsigned_webhooks: true (or LINEAR_AGENT_ALLOW_UNSIGNED_WEBHOOKS=true). When the secret is set, incoming webhook requests must include a valid Linear HMAC signature.
LINEAR_AGENT_HOME_TARGET enables cron delivery: set it to an issue ID or identifier (e.g. ENG-123) and deliver=linear_agent cron jobs post their results as comments on that issue — including when cron runs out-of-process from the gateway. Setting it also silences the gateway's one-time "no home channel" notice, which otherwise posts into each new session thread.
linear_agent:
enabled: true
webhook_host: 0.0.0.0
webhook_port: 8651
webhook_path: /hermes/linear-agent
# When set, webhooks from any OTHER workspace are ignored (one Linear app
# can be installed in several workspaces sharing a webhook secret).
workspace_id: ""
app_user_id: ""
allowed_teams: []
allowed_users: []
allow_all_users: false
ack_on_created: true
# Move an issue delegated to the agent to its first "started" state
# (Linear best practice). Default true — opt out with false. Requires
# mutation_policy.update_issues; triage-state issues are left for humans.
auto_start_on_delegation: true
# Opt-in: when auto-starting an issue that has NO delegate, also claim it
# (set the agent as delegate). Default false — humans decide what the
# agent owns unless you explicitly enable this.
auto_self_delegate: false
# Opt-in: dispatch a full agent turn for issue-update webhooks (replies
# post as issue comments). Default false — delegation already arrives as
# a real agent session, so update webhooks only feed auto-start.
dispatch_issue_updates: false
# Opt-in: when mentioned inside an existing comment thread, also post the
# findings as a reply on that source comment (so they surface in the human
# thread, not only the session widget). Requires mutation_policy.create_comments.
reply_in_source_thread: false
# Skill names auto-loaded into every Linear session (optional).
auto_skills: []
# Webhook body size cap in bytes (default 1 MiB).
max_body_bytes: 1048576
# Every write operation fails closed; enable only what you need.
mutation_policy:
create_comments: false
update_comments: false # editing an existing comment (commentUpdate)
update_issues: false # also covers issue relations + URL links
create_issues: false
# update_projects also covers project (status) updates, milestones,
# and initiatives — project-structure mutations share this key.
update_projects: false
create_documents: false
update_documents: false
create_customer_needs: false
update_customer_needs: false
# create_releases/update_releases also gate release NOTES (the
# release-family umbrella, like update_projects covers status updates).
create_releases: false
update_releases: false
create_customers: false # customer (business entity) create
update_customers: false # customer update
create_labels: false # issue label create (team or workspace)
# Deletes are a separate fail-closed family (never implied by update/create).
delete_comments: false
delete_customer_needs: false
delete_status_updates: false
delete_attachments: false
delete_customers: falseKeep OAuth client secrets and webhook secrets in .env; runtime OAuth access/refresh tokens belong in the plugin-owned state file. The state file is atomically written, process-locked, and mode 0600 on POSIX. Do not put secrets in config.yaml or docs.
hermes gateway startPoint Linear's Agent Session webhook at:
https://your-public-host.example/hermes/linear-agent
For local development, expose the configured port with a tunnel and use the tunnel HTTPS URL.
- Normal Hermes final responses are sent as Linear
responseactivities. For comment-mention sessions Linear mirrors the response into the session's thread; mentioning the agent inside an existing thread makes Linear re-anchor the conversation at a new root comment, so the reply appears there rather than in the original thread. - New sessions optionally receive a quick
thoughtacknowledgement. - Dispatch failures are reported as Linear
erroractivities. - All write tools (issues, comments, projects, status updates, milestones, initiatives, documents, customer needs, releases) are gated by
mutation_policyand fail closed by default. Agent-session activities (responses/thoughts/errors) are the core protocol and are not gated. - Set
LINEAR_AGENT_APP_USER_ID(orapp_user_id) so issue-update webhooks triggered by the agent's own mutations are ignored — without it, every write the agent makes echoes back as a new session. When unset, the adapter auto-discovers the id from Linear'sviewerquery at connect (best-effort backstop; setting it explicitly is still recommended). - Issue-update webhooks feed auto-start only by default — delegation itself arrives as a real
createdagent session, so dispatching update webhooks as turns too would double-process every delegation. Opt in withdispatch_issue_updates: trueto react to issue edits; those turns have no agent session, so replies post as issue comments (requiresmutation_policy.create_comments: true). reply_in_source_thread: trueis an opt-in workaround for a Linear limitation: a session response can't render inline in the thread the agent was mentioned from (only the first-party@Linearassistant can), so when on, the agent also replies on the mention's source comment. Content then appears in both the session widget and the thread. Default off matches other third-party agents. Remove this flag if Linear ships native inline/source-thread replies for third-party agents (see the adapter's_reply_in_source_threadcomment for the unwind steps).linear_agent_update_issue/create_issueaccept a workflow state NAME (state: "Done") and resolve it to the requiredstateIdautomatically.- MCP parity:
update_issue/create_issuealso resolve friendly reference keys (assigneeby name/email/me,labels,project,team,cycle,milestone,delegate) to Linear's*Idfields;nullclears where Linear allows it, and raw*Idkeys still pass straight through. Ambiguous or unknown names abort BEFORE the mutation and name the lookup tool. update_issuemanages issue relations (blocks,blockedBy,relatedTo— append-only;removeBlocks/removeBlockedBy/removeRelatedToto remove;parentId;duplicateOf) and URL attachments (links: [{url, title}], append-only).blockedByis stored as the inverse ofblocks.linear_agent_create_commentalso edits an existing comment (passcomment_id, gated onupdate_comments) and posts threaded replies (parentId). Comments can target a non-issue parent — pass exactly ONE ofproject_id,project_update_id,initiative_id,initiative_update_id, ordocument_content_idinstead ofissue_id.- Customers, release notes, and issue labels:
linear_agent_save_customer(gated oncreate_customers/update_customers),linear_agent_delete_customer(delete_customers),linear_agent_save_release_note(release-family umbrella —create_releases/update_releases;pipelineIdrequired to create), andlinear_agent_create_issue_label(create_labels; omitteam_idfor a workspace-wide label). New reads (no policy):linear_agent_get_team,_get_milestone,_get_document,_get_attachment,_get_release_note,_get_agent_skill,_list_project_labels,_list_release_notes,_list_agent_skills. - Delete tools (
linear_agent_delete_comment,_delete_customer_need,_delete_status_update,_delete_attachment) require explicit IDs and are each gated by their owndelete_*policy key (all defaultfalse). Status updates route through Linear's archive mutation. - MCP migration: prefer
linear_agent_*overmcp_linear_*— every MCP Linear tool available in Linear's public GraphQL API has an equivalent here, with app attribution and fail-closed write policies on top.
- The
createdacknowledgement runs concurrently with dispatch so the webhook responds within Linear's 5-second deadline and the session is acknowledged well inside the 10-second unresponsive-marking window. - Clarifying questions post as
elicitationactivities, so the session showsawaitingInputin Linear's UI; the user's reply arrives as a normalpromptedfollow-up. - Long turns surface an ephemeral "Working on it…"
thought(rate-limited) as Linear's equivalent of a typing indicator. linear_agent_set_session_linksattaches external URLs (PRs, docs) to the session — these render in Linear and also count as session activity.linear_agent_update_planpublishes the agent's execution plan (Linear Agent Plans, technology preview) as a live checklist on the session. The plan is replaced in full on every call — the model sends every step with its current status (pending/inProgress/completed/canceled).- Disengagement respect (
stopsignal): a humanstopsignal (delivered on apromptedAgent Activity) halts the session immediately — the activity body is not dispatched as a prompt, any in-flight turn is interrupted, and a single confirmingresponseactivity is posted. No further Linear writes are made for that turn. - The GraphQL client retries once on HTTP 429 honoring
Retry-After, and routes throughLINEAR_AGENT_PROXY/standard proxy env vars when set. - Do not put real tokens or webhook secrets in
config.yamlor docs.
- Auto-start on delegation (default on, opt-out): when an issue is delegated to the agent (an assignment/update webhook, or the issue's
delegateis this app user), the adapter moves it to the team's firststartedworkflow state (lowestposition) — Linear's published best practice. Opt out withauto_start_on_delegation: false; also gated onmutation_policy.update_issues. Issues in atriagestate are left alone so humans keep control of triage, and a bare @-mention is not treated as delegation. - Self-delegation (opt-in, default off): with
auto_self_delegate: true(orLINEAR_AGENT_AUTO_SELF_DELEGATE), auto-started issues that have NO delegate are also claimed by the agent. By default the adapter never delegates issues to itself — humans decide what the agent owns. - Permission-change awareness: OAuth
revokedandteamAccessChangedwebhook events are logged loudly at WARNING (instead of being silently ignored) so a revoked token or lost team access is diagnosable.