Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ agent-slack --safe-mode message send "#general" "hello"
While safe mode is active:

- `message send` → redirected to the draft editor with the text pre-filled; you review and send from the browser. The output includes `"safe_mode": true` and `"redirected_from": "send"`, and a warning is printed to stderr. Flags the editor cannot represent (`--attach`, `--blocks`, `--schedule`, `--schedule-in`, `--reply-broadcast`) are rejected with an error instead of being silently dropped.
- `message compose` → opens the browser editor normally. In CI, where compose would skip the editor and send directly, it is blocked with an error.
- `message edit` and `message delete` → blocked with an error.
- All read operations (`get`, `list`, `search`, etc.) and reactions are unchanged.

Expand Down
2 changes: 1 addition & 1 deletion skills/agent-slack/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ If a capability named here is absent from installed help, report version skew in
- Read and search freely.
- Perform write actions only when explicitly requested: sends, edits, deletes, reactions, invitations, channel or canvas creation, mark-read operations, scheduling or canceling delivery, uploads, Later state/reminder changes, DM/group-DM creation, and `workflow run`. Workflow runs can execute downstream actions.
- For compose- or review-only requests, return proposed text without invoking Slack, or use `message draft create` to add a Slack-native draft the user can review and send (nothing is posted). `message compose` is send-capable; use it only when the user explicitly asks to open the interactive editor. In CI or another noninteractive environment, do not invoke it without separate authorization to send immediately: CI skips the editor and sends supplied text.
- With `AGENT_SLACK_SAFE_MODE=1` (or the global `--safe-mode` flag) set, safe mode is enforced at the tool level: `message send` is redirected to the draft editor and `message edit`/`message delete` are blocked. Use it when nothing should post without human review.
- With `AGENT_SLACK_SAFE_MODE=1` (or the global `--safe-mode` flag) set, safe mode is enforced at the tool level: `message send` is redirected to the draft editor, the CI `message compose` direct-send shortcut is blocked, and `message edit`/`message delete` are blocked. Use it when nothing should post without human review.

## Workflow

Expand Down
5 changes: 5 additions & 0 deletions src/cli/message-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@ export function registerMessageCommand(input: { program: Command; ctx: CliContex
{ workspace?: string; threadTs?: string },
];
try {
if (safeModeActive() && process.env.CI) {
throw new Error(
'Safe mode is active: "message compose" cannot skip the editor in CI because that would post without human review.',
);
}
const payload = await composeMessage({
ctx: input.ctx,
targetInput,
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ program
.version(getPackageVersion())
.option(
"--safe-mode",
'Human-in-the-loop enforcement: redirect "message send" to the draft editor and block "message edit"/"message delete" (also: AGENT_SLACK_SAFE_MODE=1)',
'Human-in-the-loop enforcement: redirect "message send" to the draft editor, block the CI "message compose" direct-send shortcut, and block "message edit"/"message delete" (also: AGENT_SLACK_SAFE_MODE=1)',
);

startCommandWatchdog(process.argv.slice(2));
Expand Down
45 changes: 44 additions & 1 deletion test/safe-mode.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { afterEach, describe, expect, test } from "bun:test";
import { afterEach, describe, expect, mock, test } from "bun:test";
import { Command } from "commander";
import type { CliContext } from "../src/cli/context.ts";
import { registerMessageCommand } from "../src/cli/message-command.ts";
import {
isSafeModeEnabled,
redirectSendToDraft,
Expand Down Expand Up @@ -34,6 +36,47 @@ describe("safeModeBlockedError", () => {
});
});

test("safe mode blocks CI compose before workspace or API work", async () => {
const originalCi = process.env.CI;
const originalExitCode = process.exitCode;
const originalLog = console.log;
const originalError = console.error;
const log = mock(() => {});
const error = mock(() => {});
const noWorkCtx = {
errorMessage: (err: unknown) => (err instanceof Error ? err.message : String(err)),
} as CliContext;

try {
process.env.CI = "1";
process.exitCode = 0;
console.log = log as typeof console.log;
console.error = error as typeof console.error;

const program = new Command().option("--safe-mode");
registerMessageCommand({ program, ctx: noWorkCtx });
await program.parseAsync(
["--safe-mode", "message", "compose", "C12345678", "review this first"],
{ from: "user" },
);

expect(log).not.toHaveBeenCalled();
expect(error).toHaveBeenCalledWith(
'Safe mode is active: "message compose" cannot skip the editor in CI because that would post without human review.',
);
expect(process.exitCode).toBe(1);
} finally {
if (originalCi === undefined) {
delete process.env.CI;
} else {
process.env.CI = originalCi;
}
process.exitCode = originalExitCode ?? 0;
console.log = originalLog;
console.error = originalError;
}
});

describe("redirectSendToDraft", () => {
const originalCi = process.env.CI;

Expand Down
Loading