Slack: start and steer jobs from a channel¶
Type /hangar fix the flaky upload test in a channel, or mention @hangar with the instructions, and hangar runs a job on that channel's repo. It reports back in a thread:
- when the agent starts;
- when it needs a decision;
- when it finishes, with the outcome, the agent's summary, the PR link, the verification result, and what went wrong if it failed.
Reply in that thread to follow up. The agent picks up its own conversation, on the same branch and PR.
Slack is the door for unplanned work: an alert, a question in a thread. Planned work comes through Jira's Ready for agent status, and repo chores through a GitHub label (intake.md). The Slack door is maintained but not extended beyond @hangar adr (#170).
Commands¶
| Command | |
|---|---|
/hangar <instructions> or @hangar <instructions> |
Start a job on the channel's repo |
/hangar repo=owner/repo env=NAME harness=NAME model=NAME <instructions> |
Choose the repo (any in HANGAR_GITHUB_REPOS), environment (one mapped to that repo), harness and model. Options go first. |
/hangar status JOB |
A job's state, PR and latest event (only you see the slash command's answer) |
@hangar adr [title] in a thread |
Draft an ADR from the thread as a PR (below) |
/hangar help |
Usage |
| A reply in a job's thread | Follow-up once the current turn is done, from the job's person only, or anyone allowed on a team job. Start it with fix:, redirect: or info: to classify it |
Slack doesn't allow slash commands inside threads, so a /hangar job gets a new top-level message and the thread hangs off it. An @hangar mention inside an existing thread (say, under an alert) runs the job in that thread. The parent message and earlier replies, up to 15 messages and 20 KB, go to the agent as a context file, slack-thread.md. Other people and other apps wrote that thread, so the file is marked untrusted, and the prompt tells the agent to treat it as a description of the problem, never as instructions.
@hangar adr: a thread becomes a decision record¶
Decisions made in Slack threads get lost. Mention @hangar adr Use Parquet for runs in a reply in the thread where the
decision was made, on a mapped channel, and hangar drafts an ADR from the thread and opens a PR that adds one file,
docs/decisions/NNNN-use-parquet-for-runs.md. A person other than the one who asked reviews and merges it.
- Opt in per repo. The repo needs a
docs/decisions/directory: without one hangar refuses in the thread and points to docs/templates/adr.md, the format it writes (frontmatterid,kind: decision,status: proposed,label,sourceswith the thread's permalink and content hash,author: agent:<model>,supersedes; sections Context, Decision, Options considered, Consequences, Checks). - The thread is untrusted. It reaches the agent only as
slack-thread.md, an untrusted context item (the parent and earlier replies, as for any mention), with stored secrets redacted. The instructions are fixed (src/hangar/prompts/adr.md): write that one file and change nothing else. The agent runs with egressnone, and the job is tainted from turn 1, so hangar opens the PR after the turn as a draft labelledhangar:tainted(security/taint.md); the agent never pushes or opens it. - The PR. Title
ADR: <title>, labelsagent-draftedandadr, and a body linking the thread. It never lands on the requester's approval, whatever the repo'sself_approvalsays (landing.md). - hangar checks it after the turn, as the job's verify: exactly one new file, numbered one past the highest in
docs/decisions/, named after the title, with the frontmatter and sections above. The optionalchecks:block (a fenced```yamlblock startingchecks:, trace predicates flightdeck runs nightly) must be valid YAML; hangar never evaluates it. A failure fails the verify with the reason, and the agent gets a fix round like any failing check. - Path guard. A change outside
docs/decisions/still opens, as a draft, with a warning in the review packet. - Numbering. Two ADR jobs that pick the same number are flagged in the packet; whichever merges second renumbers (a follow-up on its PR).
Set it up¶
hangar setup slack --manifest slack-app.yaml
- Create the app.
hangar setup slackwrites the app manifest with your hangar's URL filled in. At https://api.slack.com/apps, choose Create New App → From a manifest, pick the workspace and paste it. Then install the app to the workspace.
Slack checks the events URL right away. That check fails until the next step is done; you'll retry it in step 3.
2. Give hangar the secrets. Paste the Signing Secret (from Basic Information) and the Bot User OAuth Token (xoxb-…, from OAuth & Permissions) when asked. Neither is echoed. Then give the channel to take work from, and its repo. The wizard does four things:
- checks the bot token with auth.test;
- stores both secrets in the vault as slack-signing and slack-bot (they never go into sandboxes);
- saves the Slack settings in hangar's database (no config map to edit);
- sends a signed test request through hangar's Slack route.
To script it: hangar setup slack --signing-secret-file … --bot-token-file … --channel C0123ABCD=acme/app --user U0456EFGH --non-interactive. Get a channel's ID from channel name → About, at the bottom. Each channel's repo must already be in HANGAR_GITHUB_REPOS (hangar setup github), which picks its environment.
3. Finish in Slack. Under Event Subscriptions, retry the request URL; it should verify now. Then invite the app to each mapped channel with /invite @hangar. Thread replies only arrive from channels the app is in.
4. Check the setup with hangar doctor. It checks:
- that both credentials are stored;
- that the bot token works (auth.test);
- that every mapped channel points at a repo in HANGAR_GITHUB_REPOS;
- that the bot token has im:write, for inbox DMs, and who has no linked Slack account to get them.
Settings¶
| Variable | Default | |
|---|---|---|
HANGAR_SLACK_SIGNING_SECRET_CREDENTIAL |
Vault credential holding the signing secret. The Slack routes return 404 until it is set. | |
HANGAR_SLACK_BOT_TOKEN_CREDENTIAL |
Vault credential holding the bot token | |
HANGAR_SLACK_CHANNELS |
{} |
{"<channel id>": "owner/repo"}: the channels that may start jobs, and each one's default repo. An exact git URL also works, as in HANGAR_GITHUB_REPOS. |
HANGAR_SLACK_USERS |
[] |
Slack user IDs (U…, or T…/U… to pin the workspace) allowed to start jobs and follow up on team jobs. Empty means anyone in a mapped channel from the app's workspace. A linked account is always allowed. Members of other organizations (Slack Connect) count only when listed here. |
HANGAR_SLACK_TEAMS |
[] |
Other workspace IDs (T…) whose members count as your own, for Enterprise Grid orgs whose users carry another workspace's ID |
HANGAR_SLACK_API_URL |
https://slack.com/api |
How it behaves¶
- Requests are verified. Each request must carry a valid
X-Slack-Signature:v0=followed by the HMAC-SHA256 ofv0:<timestamp>:<raw body>, keyed with the signing secret and compared in constant time. Requests whoseX-Slack-Request-Timestampis more than 5 minutes off are refused with 401. - Within 3 seconds. Slack expects an answer within 3 seconds. hangar acknowledges the command or event at once. It posts to Slack, reads the thread and submits the job in the background.
- Exactly once. Slack retries an event it thinks failed (
X-Slack-Retry-Num), and a mention in a thread arrives both asapp_mentionand asmessage. hangar acts once perevent_idand once per message. The record of handled events survives a restart. An event counts as handled only once its handling finishes, so one that a dying process acknowledged but never handled runs when Slack retries it. - No replays. Slack doesn't retry slash commands, so a second copy of a signed command (same signature) inside the five-minute window is a replay: hangar answers "That request was already handled." and starts nothing.
- Other organizations. In a Slack Connect channel, messages from another organization's members carry their own workspace ID (
user_team). hangar ignores their mentions and thread replies unless they are listed inHANGAR_SLACK_USERS, so sharing a channel doesn't give a partner company an agent on your repo. (Slack doesn't let another organization run your app's slash commands at all.) - Limits. Each Slack user is a caller for rate limits and the active-job cap: by default 10 jobs or follow-ups at once, refilling at 30 a minute, and at most 10 unfinished jobs. Past a limit, hangar says so in the thread.
- Only people. hangar ignores messages from bots (its own included), edits and deletions.
- Who's asking. The requester is
slack:<team>/<user>, shown in the job's timeline, PR body and audit log. Slack jobs are fenced in like ajobstoken: only repos inHANGAR_GITHUB_REPOS, only their environments, and no choice of provider. - Who can follow up (
teamcreds.may_continue, as on GitHub and Jira). A job started from a linked Slack account (hangar person link <id> slack T…/U…) is that person's, on their own API key and budget. Only a reply from an account linked to them continues it. Any other reply is refused, naming whose job it is, an admin's too: an admin useshangar job follow-up. - Team jobs. A job started from an unlinked account is a team job: any reply allowed in the channel continues it. A reply while the agent works gets "Can't continue yet".
- Needs input. When an agent stops with a question (job state
needs_input, spec 0011 §2), hangar posts the question in the thread. A reply is the answer: an ordinary follow-up, under the same rule. - Safe output. Text hangar didn't write itself, such as the agent's summary, failure output, or your instructions echoed back, is escaped. It can't ping
@channelor turn into a link. Known secrets are redacted before anything is posted. - When posting fails. If the app isn't in the channel or the token is wrong, a slash command answers privately, through Slack's
response_url, with the reason, and no job starts. Failures to post updates are recorded on the job's timeline.
Classifying a reply¶
Under hangar's "Continuing hangar job …" answer to a thread reply, three buttons ask what the reply was: Correction (the agent got it wrong), Redirect (you changed the task) or New info (you added something it didn't know). One click records it; only corrections count against the agent in the harness metrics (steering.md). Or start the reply with fix:, redirect: or info:: it is classified on arrival, the prefix is stripped, and no buttons are shown. An answer to the agent's question counts as new info.
- Only the person who sent the reply can classify it. Anyone else's click gets a private ("only you can see this") refusal.
- Buttons come only on the acknowledgement of a person's reply, never on hangar's own status posts.
- Clicks arrive at
POST /api/hooks/slack/interactions(the manifest's Interactivity request URL). They are verified like events and commands: the signing secret, the 5-minute timestamp window, the body size cap, and a replay guard (a second copy of a signed click is ignored). An app created before this needs Interactivity & Shortcuts turned on with that URL.
Inbox DMs¶
hangar also DMs people what needs them (the inbox): each new question or approval of their agents at once, and a digest at 10:15 and 16:00. It opens the DM with conversations.open and posts with chat.postMessage, so the app needs the im:write bot scope (in the manifest; an app installed before it needs the scope added and a reinstall). hangar doctor warns when the bot token lacks it. A DM goes to a person's linked Slack account (hangar person link <id> slack T…/U…); settings per person are hangar person set <id> --dm off|questions|all --tz … --digest … --quiet … (team.md). DM text is escaped and redacted like thread posts, and links to the thread, ticket or PR where the answer is given; there are no buttons in it.
Security notes¶
Treat a mapped channel like write access to its repo's hangar/* branches. Anyone in your workspace who can post there can start an agent, unless HANGAR_SLACK_USERS is set. Messages in the thread are context for the agent, and the prompt marks them as untrusted data. Even so, they are untrusted input, like issue comments; see jobs → Security notes. A thread reply is a follow-up with its sender's authority: on a team job, any allowed user's; on a person's job, only that person's (who can follow up). Keep PR merges human-approved.
/hangar status JOB answers for any job on a repo Slack may use, including jobs started over REST or GitHub, to anyone allowed in a mapped channel. It shows the title, state, requester and latest event.