feat: add agent mode for automation scenarios (#337)

* feat: add agent mode for automation scenarios

- Add agent mode that always triggers without checking for mentions
- Implement Mode interface with support for mode-specific tool configuration
- Add getAllowedTools() and getDisallowedTools() methods to Mode interface
- Simplify tests by combining related test cases
- Update documentation and examples to include agent mode
- Fix TypeScript imports to prevent circular dependencies

Agent mode is designed for automation and workflow_dispatch scenarios
where Claude should always run without requiring trigger phrases.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Minor update to readme (from @main to @beta)

* Since workflow_dispatch isn't in the base action, update the examples accordingly

* minor formatting issue

* Update to say beta instead of main

* Fix missed tracking comment to be false

---------

Co-authored-by: km-anthropic <km-anthropic@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
km-anthropic
2025-07-24 14:53:15 -07:00
committed by GitHub
parent 94437192fa
commit c3e0ab4d6d
11 changed files with 295 additions and 54 deletions

View File

@@ -167,41 +167,79 @@ jobs:
## Inputs
| Input | Description | Required | Default |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
| `mode` | Execution mode for the action. Currently supports 'tag' (default). Future modes: 'review', 'freeform' | No | `tag` |
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
| `override_prompt` | Complete replacement of Claude's prompt with custom template (supports variable substitution) | No | - |
| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - |
| `max_turns` | Maximum number of conversation turns Claude can take (limits back-and-forth exchanges) | No | - |
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` |
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - |
| `fallback_model` | Enable automatic fallback to specified model when primary model is unavailable | No | - |
| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - |
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" |
| `disallowed_tools` | Tools that Claude should never use | No | "" |
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" |
| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" |
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML format) | No | "" |
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
| Input | Description | Required | Default |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
| `mode` | Execution mode: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation with no trigger checking) | No | `tag` |
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
| `override_prompt` | Complete replacement of Claude's prompt with custom template (supports variable substitution) | No | - |
| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - |
| `max_turns` | Maximum number of conversation turns Claude can take (limits back-and-forth exchanges) | No | - |
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` |
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - |
| `fallback_model` | Enable automatic fallback to specified model when primary model is unavailable | No | - |
| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - |
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" |
| `disallowed_tools` | Tools that Claude should never use | No | "" |
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" |
| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" |
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML format) | No | "" |
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
> **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration.
## Execution Modes
The action supports two execution modes, each optimized for different use cases:
### Tag Mode (Default)
The traditional implementation mode that responds to @claude mentions, issue assignments, or labels.
- **Triggers**: `@claude` mentions, issue assignment, label application
- **Features**: Creates tracking comments with progress checkboxes, full implementation capabilities
- **Use case**: General-purpose code implementation and Q&A
```yaml
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# mode: tag is the default
```
### Agent Mode
For automation and scheduled tasks without trigger checking.
- **Triggers**: Always runs (no trigger checking)
- **Features**: Perfect for scheduled tasks, works with `override_prompt`
- **Use case**: Maintenance tasks, automated reporting, scheduled checks
```yaml
- uses: anthropics/claude-code-action@beta
with:
mode: agent
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
override_prompt: |
Check for outdated dependencies and create an issue if any are found.
```
See [`examples/claude-modes.yml`](./examples/claude-modes.yml) for complete examples of each mode.
### Using Custom MCP Configuration
The `mcp_config` input allows you to add custom MCP (Model Context Protocol) servers to extend Claude's capabilities. These servers merge with the built-in GitHub MCP servers.

View File

@@ -26,7 +26,7 @@ inputs:
# Mode configuration
mode:
description: "Execution mode for the action. Currently only 'tag' mode is supported (traditional implementation triggered by mentions/assignments)"
description: "Execution mode for the action. Valid modes: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation with no trigger checking)"
required: false
default: "tag"

56
examples/claude-modes.yml Normal file
View File

@@ -0,0 +1,56 @@
name: Claude Mode Examples
on:
# Common events for both modes
issue_comment:
types: [created]
issues:
types: [opened, labeled]
pull_request:
types: [opened]
jobs:
# Tag Mode (Default) - Traditional implementation
tag-mode-example:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
steps:
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Tag mode (default) behavior:
# - Scans for @claude mentions in comments, issues, and PRs
# - Only acts when trigger phrase is found
# - Creates tracking comments with progress checkboxes
# - Perfect for: Interactive Q&A, on-demand code changes
# Agent Mode - Automation without triggers
agent-mode-auto-review:
# Automatically review every new PR
if: github.event_name == 'pull_request' && github.event.action == 'opened'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
steps:
- uses: anthropics/claude-code-action@beta
with:
mode: agent
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
override_prompt: |
Review this PR for code quality. Focus on:
- Potential bugs or logic errors
- Security concerns
- Performance issues
Provide specific, actionable feedback.
# Agent mode behavior:
# - NO @claude mention needed - runs immediately
# - Enables true automation (impossible with tag mode)
# - Perfect for: CI/CD integration, automatic reviews, label-based workflows

View File

@@ -840,14 +840,29 @@ export async function createPrompt(
const hasActionsReadPermission =
context.inputs.additionalPermissions.get("actions") === "read" &&
context.isPR;
// Get mode-specific tools
const modeAllowedTools = mode.getAllowedTools();
const modeDisallowedTools = mode.getDisallowedTools();
// Combine with existing allowed tools
const combinedAllowedTools = [
...context.inputs.allowedTools,
...modeAllowedTools,
];
const combinedDisallowedTools = [
...context.inputs.disallowedTools,
...modeDisallowedTools,
];
const allAllowedTools = buildAllowedToolsString(
context.inputs.allowedTools,
combinedAllowedTools,
hasActionsReadPermission,
context.inputs.useCommitSigning,
);
const allDisallowedTools = buildDisallowedToolsString(
context.inputs.disallowedTools,
context.inputs.allowedTools,
combinedDisallowedTools,
combinedAllowedTools,
);
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);

View File

@@ -55,7 +55,7 @@ async function run() {
await checkHumanActor(octokit.rest, context);
// Step 6: Create initial tracking comment (mode-aware)
// Some modes (e.g., future review/freeform modes) may not need tracking comments
// Some modes (e.g., agent mode) may not need tracking comments
let commentId: number | undefined;
let commentData:
| Awaited<ReturnType<typeof createInitialComment>>

View File

@@ -7,9 +7,8 @@ import type {
PullRequestReviewEvent,
PullRequestReviewCommentEvent,
} from "@octokit/webhooks-types";
import type { ModeName } from "../modes/registry";
import { DEFAULT_MODE } from "../modes/registry";
import { isValidMode } from "../modes/registry";
import type { ModeName } from "../modes/types";
import { DEFAULT_MODE, isValidMode } from "../modes/registry";
export type ParsedGitHubContext = {
runId: string;

42
src/modes/agent/index.ts Normal file
View File

@@ -0,0 +1,42 @@
import type { Mode } from "../types";
/**
* Agent mode implementation.
*
* This mode is designed for automation and workflow_dispatch scenarios.
* It always triggers (no checking), allows highly flexible configurations,
* and works well with override_prompt for custom workflows.
*
* In the future, this mode could restrict certain tools for safety in automation contexts,
* e.g., disallowing WebSearch or limiting file system operations.
*/
export const agentMode: Mode = {
name: "agent",
description: "Automation mode that always runs without trigger checking",
shouldTrigger() {
return true;
},
prepareContext(context, data) {
return {
mode: "agent",
githubContext: context,
commentId: data?.commentId,
baseBranch: data?.baseBranch,
claudeBranch: data?.claudeBranch,
};
},
getAllowedTools() {
return [];
},
getDisallowedTools() {
return [];
},
shouldCreateTrackingComment() {
return false;
},
};

View File

@@ -5,17 +5,17 @@
*
* To add a new mode:
* 1. Add the mode name to VALID_MODES below
* 2. Create the mode implementation in a new directory (e.g., src/modes/review/)
* 2. Create the mode implementation in a new directory (e.g., src/modes/new-mode/)
* 3. Import and add it to the modes object below
* 4. Update action.yml description to mention the new mode
*/
import type { Mode } from "./types";
import { tagMode } from "./tag/index";
import type { Mode, ModeName } from "./types";
import { tagMode } from "./tag";
import { agentMode } from "./agent";
export const DEFAULT_MODE = "tag" as const;
export const VALID_MODES = ["tag"] as const;
export type ModeName = (typeof VALID_MODES)[number];
export const VALID_MODES = ["tag", "agent"] as const;
/**
* All available modes.
@@ -23,6 +23,7 @@ export type ModeName = (typeof VALID_MODES)[number];
*/
const modes = {
tag: tagMode,
agent: agentMode,
} as const satisfies Record<ModeName, Mode>;
/**

View File

@@ -1,5 +1,6 @@
import type { ParsedGitHubContext } from "../github/context";
import type { ModeName } from "./registry";
export type ModeName = "tag" | "agent";
export type ModeContext = {
mode: ModeName;
@@ -20,9 +21,9 @@ export type ModeData = {
* Each mode defines its own behavior for trigger detection, prompt generation,
* and tracking comment creation.
*
* Future modes might include:
* - 'review': Optimized for code reviews without tracking comments
* - 'freeform': For automation with no trigger checking
* Current modes include:
* - 'tag': Traditional implementation triggered by mentions/assignments
* - 'agent': For automation with no trigger checking
*/
export type Mode = {
name: ModeName;
@@ -39,13 +40,12 @@ export type Mode = {
prepareContext(context: ParsedGitHubContext, data?: ModeData): ModeContext;
/**
* Returns additional tools that should be allowed for this mode
* (base GitHub tools are always included)
* Returns the list of tools that should be allowed for this mode
*/
getAllowedTools(): string[];
/**
* Returns tools that should be disallowed for this mode
* Returns the list of tools that should be disallowed for this mode
*/
getDisallowedTools(): string[];

82
test/modes/agent.test.ts Normal file
View File

@@ -0,0 +1,82 @@
import { describe, test, expect, beforeEach } from "bun:test";
import { agentMode } from "../../src/modes/agent";
import type { ParsedGitHubContext } from "../../src/github/context";
import { createMockContext } from "../mockContext";
describe("Agent Mode", () => {
let mockContext: ParsedGitHubContext;
beforeEach(() => {
mockContext = createMockContext({
eventName: "workflow_dispatch",
isPR: false,
});
});
test("agent mode has correct properties and behavior", () => {
// Basic properties
expect(agentMode.name).toBe("agent");
expect(agentMode.description).toBe(
"Automation mode that always runs without trigger checking",
);
expect(agentMode.shouldCreateTrackingComment()).toBe(false);
// Tool methods return empty arrays
expect(agentMode.getAllowedTools()).toEqual([]);
expect(agentMode.getDisallowedTools()).toEqual([]);
// Always triggers regardless of context
const contextWithoutTrigger = createMockContext({
eventName: "workflow_dispatch",
isPR: false,
inputs: {
...createMockContext().inputs,
triggerPhrase: "@claude",
},
payload: {} as any,
});
expect(agentMode.shouldTrigger(contextWithoutTrigger)).toBe(true);
});
test("prepareContext includes all required data", () => {
const data = {
commentId: 789,
baseBranch: "develop",
claudeBranch: "claude/automated-task",
};
const context = agentMode.prepareContext(mockContext, data);
expect(context.mode).toBe("agent");
expect(context.githubContext).toBe(mockContext);
expect(context.commentId).toBe(789);
expect(context.baseBranch).toBe("develop");
expect(context.claudeBranch).toBe("claude/automated-task");
});
test("prepareContext works without data", () => {
const context = agentMode.prepareContext(mockContext);
expect(context.mode).toBe("agent");
expect(context.githubContext).toBe(mockContext);
expect(context.commentId).toBeUndefined();
expect(context.baseBranch).toBeUndefined();
expect(context.claudeBranch).toBeUndefined();
});
test("agent mode triggers for all event types", () => {
const events = [
"push",
"schedule",
"workflow_dispatch",
"repository_dispatch",
"issue_comment",
"pull_request",
];
events.forEach((eventName) => {
const context = createMockContext({ eventName, isPR: false });
expect(agentMode.shouldTrigger(context)).toBe(true);
});
});
});

View File

@@ -1,6 +1,8 @@
import { describe, test, expect } from "bun:test";
import { getMode, isValidMode, type ModeName } from "../../src/modes/registry";
import { getMode, isValidMode } from "../../src/modes/registry";
import type { ModeName } from "../../src/modes/types";
import { tagMode } from "../../src/modes/tag";
import { agentMode } from "../../src/modes/agent";
describe("Mode Registry", () => {
test("getMode returns tag mode by default", () => {
@@ -9,20 +11,26 @@ describe("Mode Registry", () => {
expect(mode.name).toBe("tag");
});
test("getMode returns agent mode", () => {
const mode = getMode("agent");
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
test("getMode throws error for invalid mode", () => {
const invalidMode = "invalid" as unknown as ModeName;
expect(() => getMode(invalidMode)).toThrow(
"Invalid mode 'invalid'. Valid modes are: 'tag'. Please check your workflow configuration.",
"Invalid mode 'invalid'. Valid modes are: 'tag', 'agent'. Please check your workflow configuration.",
);
});
test("isValidMode returns true for tag mode", () => {
test("isValidMode returns true for all valid modes", () => {
expect(isValidMode("tag")).toBe(true);
expect(isValidMode("agent")).toBe(true);
});
test("isValidMode returns false for invalid mode", () => {
expect(isValidMode("invalid")).toBe(false);
expect(isValidMode("review")).toBe(false);
expect(isValidMode("freeform")).toBe(false);
});
});