Compare commits

..

2 Commits

Author SHA1 Message Date
Ed Zynda 9c3751623f fix(cmd): harden kit github install workflow and secret handling
- Pass the provider secret to `gh secret set` via stdin instead of the
  --body flag so the API key never appears in the process argument list
- Gate the generated workflow on author_association (OWNER, MEMBER,
  COLLABORATOR) so untrusted users cannot trigger privileged runs
- Match `/kit` only as a leading command token instead of an incidental
  substring anywhere in the comment body
- Thread cmd.Context() through to the gh invocation
- Update tests and docs to reflect the refined trigger conditions
2026-06-15 23:26:44 +03:00
Ed Zynda 41ab071f14 feat(cmd): add kit github install command (#60)
- Add `kit github` parent command and `kit github install` subcommand
  that scaffolds .github/workflows/kit.yml to run Kit as a GitHub
  Actions collaborator/reviewer triggered by `/kit` comments
- Generate a least-privilege workflow with persist-credentials: false,
  resolve the provider secret env var from the model registry, and
  refuse to clobber an existing file unless --force
- Offer to set the provider secret via the gh CLI when available;
  flags: --model, --force, --no-secret
- Add unit tests for secret resolution, workflow rendering, and write
- Document the command in README and the docs site (cli/commands, index)

Fixes #60
2026-06-15 22:17:35 +03:00
5 changed files with 429 additions and 0 deletions
+42
View File
@@ -28,6 +28,7 @@ A powerful, extensible AI coding agent CLI with multi-provider support, built-in
- **Interactive TUI**: Rich terminal interface powered by Bubble Tea with streaming, syntax highlighting, and custom rendering
- **Session Management**: Tree-based conversation history with branching support
- **Non-Interactive Mode**: Script-friendly positional args with JSON output
- **GitHub Integration**: Scaffold a GitHub Actions workflow with `kit github install` to run Kit as a collaborator/reviewer on `/kit` comments
- **ACP Server**: Run Kit as an [Agent Client Protocol](https://agentclientprotocol.com) agent over stdio
- **Go SDK**: Embed Kit in your own applications with full agent lifecycle events (30+ event types) and behavior-modifying hooks
@@ -260,6 +261,12 @@ kit install --uninstall <pkg> # Remove an installed package
# Skills
kit skill # Install the Kit extensions skill via skills.sh
# GitHub integration
kit github install # Scaffold .github/workflows/kit.yml (run Kit on '/kit' comments)
kit github install --model anthropic/claude-sonnet-4-5-20250929
kit github install --force # Overwrite an existing workflow file
kit github install --no-secret # Skip the offer to set the provider secret via the gh CLI
# ACP server
kit acp # Start as ACP agent (stdio JSON-RPC)
kit acp --debug # With debug logging to stderr
@@ -478,6 +485,41 @@ Placeholders inside fenced code blocks (```) and inline code spans are ignored.
Disable templates with `--no-prompt-templates` or load a specific template with `--prompt-template <name>`.
## GitHub Integration
Kit can run as an automated collaborator/reviewer inside GitHub Actions. The
`kit github install` command scaffolds a workflow that triggers when someone
comments `/kit ...` on an issue or pull request review, runs the agent
non-interactively in the runner, and lets it respond.
```bash
kit github install
```
This writes `.github/workflows/kit.yml`. By default the command prompts for the
model (pre-filled with a sensible default); pass `--model` to skip the prompt.
If the [`gh` CLI](https://cli.github.com/) is detected on your `PATH` and the
provider API key is present in your environment, you'll be offered the option to
store it as a repository secret automatically.
The generated workflow:
- Triggers only on `issue_comment` and `pull_request_review_comment` (`types: [created]`).
- Runs only when the comment begins with the `/kit` command token.
- Restricts triggers to repository owners, members, and collaborators (via `author_association`).
- Uses least-privilege `permissions` and `persist-credentials: false`.
- Authenticates git/PR operations with the built-in `secrets.GITHUB_TOKEN` and
the provider via a repository secret (e.g. `ANTHROPIC_API_KEY`).
After committing the workflow and setting the provider secret, comment
`/kit <your request>` on any issue or pull request to trigger Kit.
| Flag | Description |
| --- | --- |
| `--model` | Provider/model to write into the workflow |
| `--force` | Overwrite an existing workflow file |
| `--no-secret` | Skip the offer to set the provider secret via the `gh` CLI |
## Session Management
Kit uses a tree-based session model that supports branching and forking conversations.
+255
View File
@@ -0,0 +1,255 @@
package cmd
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"charm.land/huh/v2"
"github.com/charmbracelet/log"
kit "github.com/mark3labs/kit/pkg/kit"
"github.com/spf13/cobra"
)
// defaultGitHubModel is the model written into the generated workflow when the
// user does not specify one and runs non-interactively.
const defaultGitHubModel = "anthropic/claude-sonnet-4-5-20250929"
// githubWorkflowPath is the repository-relative location of the generated
// GitHub Actions workflow that wires Kit into a repository as a collaborator.
const githubWorkflowPath = ".github/workflows/kit.yml"
var (
githubInstallModel string
githubInstallForce bool
githubInstallNoSecret bool
)
// githubCmd is the parent command for GitHub integration subcommands. It groups
// the turnkey setup tooling that wires Kit into a repository as an automated
// collaborator/reviewer driven by GitHub Actions.
var githubCmd = &cobra.Command{
Use: "github",
Short: "Set up Kit as a GitHub collaborator/reviewer",
Long: `Set up Kit as an automated collaborator/reviewer in a GitHub repository.
Kit runs inside a GitHub Actions runner, reads the relevant context (an issue
thread or pull request), runs the agent non-interactively, and responds by
posting comments and opening pull requests.
Use 'kit github install' to scaffold the GitHub Actions workflow.`,
}
// githubInstallCmd scaffolds the GitHub Actions workflow that runs Kit on
// '/kit' comment triggers. It writes .github/workflows/kit.yml and, when the
// 'gh' CLI is available, offers to set the provider API key as a repository
// secret.
var githubInstallCmd = &cobra.Command{
Use: "install",
Short: "Scaffold the GitHub Actions workflow that runs Kit",
Long: `Scaffold the GitHub Actions workflow that runs Kit as a collaborator.
This writes .github/workflows/kit.yml configured to trigger when someone
comments '/kit ...' on an issue or pull request review. The workflow runs Kit
inside an ephemeral Actions runner with least-privilege permissions and
'persist-credentials: false', mirroring established security practice.
If the GitHub CLI ('gh') is detected on your PATH, you will be offered the
option to store your provider API key as a repository secret automatically.
Flags:
--model Provider/model to write into the workflow (e.g. anthropic/claude-sonnet-4-5)
--force Overwrite an existing workflow file
--no-secret Skip the offer to set the provider secret via the gh CLI
Examples:
kit github install
kit github install --model anthropic/claude-sonnet-4-5-20250929
kit github install --force --no-secret`,
Args: cobra.NoArgs,
RunE: runGitHubInstall,
}
func init() {
githubInstallCmd.Flags().StringVarP(&githubInstallModel, "model", "m", "", "provider/model to write into the workflow")
githubInstallCmd.Flags().BoolVar(&githubInstallForce, "force", false, "overwrite an existing workflow file")
githubInstallCmd.Flags().BoolVar(&githubInstallNoSecret, "no-secret", false, "skip setting the provider secret via the gh CLI")
githubCmd.AddCommand(githubInstallCmd)
rootCmd.AddCommand(githubCmd)
}
func runGitHubInstall(cmd *cobra.Command, _ []string) error {
model, err := resolveGitHubModel()
if err != nil {
return err
}
provider, _, err := kit.ParseModelString(model)
if err != nil {
return fmt.Errorf("invalid model %q: %w", model, err)
}
secretName := providerSecretEnvVar(provider)
if err := writeGitHubWorkflow(model, secretName, githubInstallForce); err != nil {
return err
}
fmt.Printf("✅ Wrote %s\n", githubWorkflowPath)
maybeSetProviderSecret(cmd.Context(), secretName)
printGitHubInstallNextSteps(secretName)
log.Info("github workflow scaffolded", "model", model, "secret", secretName)
return nil
}
// resolveGitHubModel determines the model to embed in the workflow. The
// --model flag takes precedence; otherwise an interactive prompt is shown
// (pre-filled with the default), and non-interactive runs use the default.
func resolveGitHubModel() (string, error) {
if githubInstallModel != "" {
return strings.TrimSpace(githubInstallModel), nil
}
if !isInteractive() {
return defaultGitHubModel, nil
}
model := defaultGitHubModel
err := huh.NewInput().
Title("Model").
Description("Provider/model Kit should use in CI (e.g. anthropic/claude-sonnet-4-5)").
Value(&model).
Run()
if err != nil {
return "", fmt.Errorf("model selection cancelled: %w", err)
}
model = strings.TrimSpace(model)
if model == "" {
return "", fmt.Errorf("model cannot be empty")
}
return model, nil
}
// providerSecretEnvVar returns the environment variable / repository secret
// name that holds the API key for the given provider. It consults the model
// registry and falls back to "<PROVIDER>_API_KEY" for unknown providers.
func providerSecretEnvVar(provider string) string {
if info := kit.GetProviderInfo(provider); info != nil && len(info.Env) > 0 {
return info.Env[0]
}
sanitized := strings.ToUpper(strings.NewReplacer("-", "_", ".", "_").Replace(provider))
return sanitized + "_API_KEY"
}
// renderGitHubWorkflow builds the workflow YAML for the given model and
// provider secret name.
func renderGitHubWorkflow(model, secretName string) string {
return fmt.Sprintf(`name: kit
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
kit:
if: |
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR') &&
(startsWith(github.event.comment.body, '/kit ') ||
github.event.comment.body == '/kit')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: mark3labs/kit-action@v1
with:
model: %s
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
%s: ${{ secrets.%s }}
`, model, secretName, secretName)
}
// writeGitHubWorkflow writes the generated workflow to githubWorkflowPath,
// creating parent directories as needed. It refuses to overwrite an existing
// file unless force is true.
func writeGitHubWorkflow(model, secretName string, force bool) error {
if _, err := os.Stat(githubWorkflowPath); err == nil && !force {
return fmt.Errorf("%s already exists; re-run with --force to overwrite", githubWorkflowPath)
} else if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("checking %s: %w", githubWorkflowPath, err)
}
if err := os.MkdirAll(filepath.Dir(githubWorkflowPath), 0o755); err != nil {
return fmt.Errorf("creating %s: %w", filepath.Dir(githubWorkflowPath), err)
}
content := renderGitHubWorkflow(model, secretName)
if err := os.WriteFile(githubWorkflowPath, []byte(content), 0o644); err != nil {
return fmt.Errorf("writing %s: %w", githubWorkflowPath, err)
}
return nil
}
// maybeSetProviderSecret offers to set the provider API key as a repository
// secret via the gh CLI when it is available, interactive, the secret value is
// present in the environment, and the user did not pass --no-secret.
func maybeSetProviderSecret(ctx context.Context, secretName string) {
if githubInstallNoSecret || !isInteractive() {
return
}
if _, err := exec.LookPath("gh"); err != nil {
return
}
value := os.Getenv(secretName)
if value == "" {
fmt.Printf("️ %s is not set in your environment; set the repository secret manually with:\n", secretName)
fmt.Printf(" gh secret set %s\n", secretName)
return
}
var confirm bool
if err := huh.NewConfirm().
Title(fmt.Sprintf("Set the %s repository secret via gh?", secretName)).
Description("Uses the value from your current environment.").
Value(&confirm).
Run(); err != nil || !confirm {
return
}
// Feed the secret value via stdin rather than a command-line argument so
// the API key never appears in the process argument list.
cmd := exec.CommandContext(ctx, "gh", "secret", "set", secretName)
cmd.Stdin = strings.NewReader(value)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("⚠️ Failed to set secret via gh: %v\n", err)
fmt.Printf(" Set it manually with: gh secret set %s\n", secretName)
return
}
fmt.Printf("✅ Set repository secret %s\n", secretName)
}
// printGitHubInstallNextSteps prints the manual follow-up actions a user must
// take after the workflow is scaffolded.
func printGitHubInstallNextSteps(secretName string) {
fmt.Println("\nNext steps:")
fmt.Printf(" 1. Commit the workflow: git add %s && git commit -m \"ci: add kit workflow\"\n", githubWorkflowPath)
fmt.Printf(" 2. Set the %s repository secret (Settings → Secrets → Actions), if not already set.\n", secretName)
fmt.Println(" 3. Comment '/kit <your request>' on an issue or pull request to trigger Kit.")
}
+102
View File
@@ -0,0 +1,102 @@
package cmd
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestProviderSecretEnvVar(t *testing.T) {
tests := []struct {
provider string
want string
}{
{"anthropic", "ANTHROPIC_API_KEY"},
{"openai", "OPENAI_API_KEY"},
// Unknown provider falls back to "<PROVIDER>_API_KEY" with sanitization.
{"my-custom.provider", "MY_CUSTOM_PROVIDER_API_KEY"},
}
for _, tt := range tests {
t.Run(tt.provider, func(t *testing.T) {
got := providerSecretEnvVar(tt.provider)
if got != tt.want {
t.Errorf("providerSecretEnvVar(%q) = %q, want %q", tt.provider, got, tt.want)
}
})
}
}
func TestRenderGitHubWorkflow(t *testing.T) {
out := renderGitHubWorkflow("anthropic/claude-sonnet-4-5-20250929", "ANTHROPIC_API_KEY")
wantSubstrings := []string{
"name: kit",
"issue_comment:",
"pull_request_review_comment:",
"startsWith(github.event.comment.body, '/kit ')",
"github.event.comment.body == '/kit'",
"github.event.comment.author_association == 'OWNER'",
"github.event.comment.author_association == 'COLLABORATOR'",
"persist-credentials: false",
"uses: mark3labs/kit-action@v1",
"model: anthropic/claude-sonnet-4-5-20250929",
"GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}",
"ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}",
"contents: write",
"pull-requests: write",
"issues: write",
}
for _, want := range wantSubstrings {
if !strings.Contains(out, want) {
t.Errorf("rendered workflow missing %q\n---\n%s", want, out)
}
}
}
func TestWriteGitHubWorkflow(t *testing.T) {
dir := t.TempDir()
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(cwd) })
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
// First write succeeds and creates nested directories.
if err := writeGitHubWorkflow("anthropic/claude-sonnet-4-5", "ANTHROPIC_API_KEY", false); err != nil {
t.Fatalf("writeGitHubWorkflow: %v", err)
}
data, err := os.ReadFile(githubWorkflowPath)
if err != nil {
t.Fatalf("reading workflow: %v", err)
}
if !strings.Contains(string(data), "model: anthropic/claude-sonnet-4-5") {
t.Errorf("workflow missing model line:\n%s", data)
}
// Second write without force must refuse to clobber.
if err := writeGitHubWorkflow("anthropic/claude-sonnet-4-5", "ANTHROPIC_API_KEY", false); err == nil {
t.Error("expected error when overwriting without --force, got nil")
}
// With force it overwrites.
if err := writeGitHubWorkflow("openai/gpt-5", "OPENAI_API_KEY", true); err != nil {
t.Fatalf("writeGitHubWorkflow with force: %v", err)
}
data, err = os.ReadFile(githubWorkflowPath)
if err != nil {
t.Fatalf("reading workflow: %v", err)
}
if !strings.Contains(string(data), "OPENAI_API_KEY") {
t.Errorf("forced overwrite did not update content:\n%s", data)
}
// Sanity: the file lives at the expected nested path.
if _, err := os.Stat(filepath.Join(dir, githubWorkflowPath)); err != nil {
t.Errorf("workflow not at expected path: %v", err)
}
}
+29
View File
@@ -76,6 +76,35 @@ kit --no-skills "prompt"
Skills are auto-discovered from `~/.config/kit/skills/`, `.kit/skills/`, and `.agents/skills/` by default. Use `--skills-dir` to override the project-local search root, or `--skill` to load files explicitly (which disables auto-discovery). `--no-skills` suppresses all skill loading regardless of other flags.
## GitHub integration
Scaffold a GitHub Actions workflow that runs Kit as an automated collaborator/reviewer. The workflow triggers when someone comments `/kit ...` on an issue or pull request review, runs the agent non-interactively in the runner, and lets it respond.
```bash
kit github install # Scaffold .github/workflows/kit.yml
kit github install --model anthropic/claude-sonnet-4-5-20250929 # Skip the model prompt
kit github install --force # Overwrite an existing workflow file
kit github install --no-secret # Skip the offer to set the provider secret via the gh CLI
```
By default the command prompts for the model (pre-filled with a sensible default). If the [`gh` CLI](https://cli.github.com/) is detected on your `PATH` and the provider API key is present in your environment, you'll be offered the option to store it as a repository secret automatically.
The generated workflow:
- Triggers only on `issue_comment` and `pull_request_review_comment` (`types: [created]`).
- Runs only when the comment begins with the `/kit` command token.
- Restricts triggers to repository owners, members, and collaborators (via `author_association`).
- Uses least-privilege `permissions` and `persist-credentials: false`.
- Authenticates git/PR operations with the built-in `secrets.GITHUB_TOKEN` and the provider via a repository secret (e.g. `ANTHROPIC_API_KEY`).
After committing the workflow and setting the provider secret, comment `/kit <your request>` on any issue or pull request to trigger Kit.
| Flag | Description |
|------|-------------|
| `--model` | Provider/model to write into the workflow |
| `--force` | Overwrite an existing workflow file |
| `--no-secret` | Skip the offer to set the provider secret via the `gh` CLI |
## Interactive slash commands
These commands are available inside the Kit TUI during an interactive session:
+1
View File
@@ -20,6 +20,7 @@ A powerful, extensible AI coding agent CLI with multi-provider support, built-in
- **Interactive TUI** — Rich terminal interface powered by Bubble Tea with streaming, syntax highlighting, and custom rendering
- **Session Management** — Tree-based conversation history with branching support
- **Non-Interactive Mode** — Script-friendly positional args with JSON output
- **GitHub Integration** — Scaffold a GitHub Actions workflow with `kit github install` to run Kit as a collaborator/reviewer on `/kit` comments
- **ACP Server** — Run Kit as an [Agent Client Protocol](https://agentclientprotocol.com) agent over stdio
- **Go SDK** — Embed Kit in your own applications