Skip to content
Merged
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
7 changes: 4 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,11 @@ jobs:

caveats <<~EOS
To configure slck, run:
slck config set-token
slck init

On macOS, your token is stored securely in the system Keychain.
On Linux, your token is stored in ~/.config/slack-chat-api/credentials.
Tokens are stored in the OS keyring (Keychain on macOS,
Credential Manager on Windows, Secret Service on Linux).
They are never written to a plaintext file.
EOS
end
CASKEOF
Expand Down
18 changes: 15 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,28 @@ The `internal/client` package wraps the Slack API:

## Common Issues

- **Token not found**: Run `slck config set-token` or set `SLACK_API_TOKEN`
- **Token not found**: Run `slck init` or `slck set-credential --key bot_token --stdin`. Environment variables are NOT read at runtime (only as setup ingress, e.g. `init --bot-token-from-env`).
- **Permission denied**: Check bot token scopes in Slack app settings
- **Lint failures**: Run `make lint` locally before pushing
- **golangci-lint version**: CI uses v2.0.2 with v2 config format

## Credentials

slck stores credentials in the OS keyring via `cli-common/credstore` (Open
CLI Collective Secret-Handling Standard §2.4). The `internal/keychain`
package is a credstore adapter (no `security` shell-out, no plaintext file).
Non-secret config (`credential_ref`, `workspace`, `keyring.backend`) lives in
`~/.config/slack-chat-api/config.yml`. Ingress is only `slck init` /
`slck set-credential` (stdin or `--from-env`); never a flag/positional value.

## Dependencies

- `github.com/slack-go/slack` - Slack API client
- `github.com/open-cli-collective/cli-common` - shared credstore (OS keyring)
- `github.com/spf13/cobra` - CLI framework
- `github.com/zalando/go-keyring` - Cross-platform keychain
- `golang.org/x/term` - no-echo passphrase prompt (file-backend opt-in)

(The HTTP Slack client is hand-rolled in `internal/client`; there is no
`slack-go`/`zalando` dependency.)

## Commit Conventions

Expand Down
126 changes: 74 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,22 +129,31 @@ make build

## Platform Support

Credentials are stored in the OS keyring via the shared `cli-common/credstore`
library:

| Platform | Credential Storage |
|----------|-------------------|
| macOS | Secure (Keychain) |
| Linux | Config file (`~/.config/slack-chat-api/credentials`) |
| Windows | Config file (`%USERPROFILE%\.config\slack-chat-api\credentials`) |
| macOS | Keychain |
| Windows | Credential Manager |
| Linux | Secret Service (D-Bus); fails closed if a working keyring is locked. Encrypted-file backend only when there is no keyring at all, or by explicit opt-in (`keyring.backend: file` in `config.yml`). |

**Note:** On Linux and Windows, credentials are stored in a file with restricted permissions (0600). While not as secure as macOS Keychain, this is standard practice for CLI tools.
Tokens are **never** written to a plaintext file. Non-secret config
(`credential_ref`, `workspace`) lives in `~/.config/slack-chat-api/config.yml`.

### Credential Resolution

Credentials are resolved in this order (first match wins):

1. **Environment variables** (highest priority) — `SLACK_API_TOKEN`, `SLACK_USER_TOKEN`
2. **Stored credentials** — Keychain (macOS) or config file (Linux/Windows)
At runtime the **only** source of a token is the OS keyring. Environment
variables are **not** read as credentials at runtime (per the Open CLI
Collective Secret-Handling Standard §1.11) — they are accepted only as
*ingress* during setup, e.g. `slck init --bot-token-from-env SLACK_BOT_TOKEN`
or `op read ... | slck set-credential --key bot_token --stdin`.

This means environment variables always override stored credentials, allowing automation tools to inject their own tokens without conflicting with locally stored ones.
> **Migrating from an older slck?** On first run, any tokens from a previous
> version (old macOS Keychain items under service `slck`/`slack-chat-api`, or
> the legacy `~/.config/slack-chat-api/credentials` file) are moved into the
> keyring automatically, once, then the originals are removed. A one-line
> notice is printed to stderr (and a `_migration` block to JSON output).

## Authentication

Expand Down Expand Up @@ -303,47 +312,47 @@ This means environment variables always override stored credentials, allowing au
}
```

**Upgrading an existing install?** Paste the full manifest above into your app's **Features → App Manifest** tab (replacing the previous manifest), click **Save Changes**, then click **Install App → Reinstall to Workspace** to grant the new scopes. Copy the fresh `xoxb-…` token and run `slck config set-token`.
**Upgrading an existing install?** Paste the full manifest above into your app's **Features → App Manifest** tab (replacing the previous manifest), click **Save Changes**, then click **Install App → Reinstall to Workspace** to grant the new scopes. Copy the fresh `xoxb-…` token and run `slck init` (or `slck set-credential`).
</details>
4. Click **Create** → **Install to Workspace** → **Allow**
5. Copy the **Bot User OAuth Token** (starts with `xoxb-`)
6. Run:
6. Run the interactive setup:
```bash
slck config set-token
# Paste your token when prompted
slck init
# Paste your token when prompted (input is not echoed back)
```

Your token is stored securely in macOS Keychain, or in a config file on Linux and Windows.
Your token is stored in the OS keyring (Keychain / Credential Manager /
Secret Service). It is never written to a plaintext file.

**NOTE:** If you plan on sending messages or taking actions using your user token _(See: Choosing Between Bot and User Tokens)_, you'll need to adjust the manifest above to have all the same scopes configured for your user as your bot (with the exception of the `"channels:manage"` scope, which only applies to bots).

### Alternative: Environment Variable

```bash
export SLACK_API_TOKEN=xoxb-your-token-here
```

### Alternative: 1Password Integration
### Scripted / non-interactive setup

Use a shell function to lazy-load your token from 1Password on first use:
The token is read only from stdin or a named env var — never a flag or
positional value (so it can't leak via shell history or `ps`):

```bash
# Add to ~/.zshrc or ~/.bashrc
slack-chat() {
if [[ -z "$SLACK_API_TOKEN" ]]; then
export SLACK_API_TOKEN="$(op read 'op://Personal/slck/api_token')"
fi
command slck "$@"
}
# From a pipe (preferred for automation):
op read 'op://Personal/slck/bot_token' | slck set-credential --key bot_token --stdin

# Or, during full init, from a named env var (the env var is consumed at
# setup time only; it is NOT read on subsequent runs):
slck init --bot-token-from-env SLACK_BOT_TOKEN --user-token-from-env SLACK_USER_TOKEN
```

Or create an alias that always fetches fresh:
### 1Password integration

Because the token lives in the keyring after setup, you do **not** wrap every
invocation in `op`. Stage it once:

```bash
alias slack-chat='SLACK_API_TOKEN="$(op read '\''op://Personal/slck/api_token'\'')" slck'
op read 'op://Personal/slck/bot_token' | slck set-credential --key bot_token --stdin
op read 'op://Personal/slck/user_token' | slck set-credential --key user_token --stdin
```

Replace `op://Personal/slck/api_token` with your 1Password secret reference.
Replace the `op://…` references with your own 1Password secret references.
Re-run only when the token rotates.

### Required Scopes

Expand Down Expand Up @@ -394,14 +403,14 @@ Most commands use the **bot token**. Search commands require a **user token**.
**Setting up both tokens:**

```bash
# Set bot token (for channels, users, messages, workspace)
slck config set-token xoxb-your-bot-token
# Bot token (for channels, users, messages, workspace)
op read 'op://Personal/slck/bot_token' | slck set-credential --key bot_token --stdin

# Set user token (for search)
slck config set-token xoxp-your-user-token
# User token (for search)
op read 'op://Personal/slck/user_token' | slck set-credential --key user_token --stdin
```

The `set-token` command automatically detects the token type and stores it appropriately.
Or run `slck init` for a guided, interactive setup of both.

**Getting a user token:**

Expand All @@ -410,12 +419,12 @@ The `set-token` command automatically detects the token type and stores it appro
3. Reinstall app to workspace (if already installed)
4. Copy the **User OAuth Token** (starts with `xoxp-`)

**Environment variables:**
**Setup-time env-var ingress** (read once during `slck init`, never at runtime):

| Variable | Token Type | Description |
|----------|------------|-------------|
| `SLACK_API_TOKEN` | Bot | Bot token for most commands |
| `SLACK_USER_TOKEN` | User | User token for search commands |
| Flag | Description |
|------|-------------|
| `slck init --bot-token-from-env NAME` | Read the bot token from env var `NAME` at setup |
| `slck init --user-token-from-env NAME` | Read the user token from env var `NAME` at setup |

## Global Flags

Expand Down Expand Up @@ -743,25 +752,34 @@ slck whoami
### Config

```bash
# Set API token (interactive prompt)
slck config set-token
# Guided interactive setup (bot + optional user token)
slck init

# Set API token directly
slck config set-token xoxb-your-token-here
# Set one credential from stdin (scriptable; no value on the command line)
op read 'op://Personal/slck/bot_token' | slck set-credential --key bot_token --stdin

# Show current config status
# Show current config status (backend, ref, which keys present — never values)
slck config show

# Delete stored token
# Delete stored token(s)
slck config delete-token
```

#### Config Command Reference

Ingress is at the top level, not under `config`:

| Command | Flags | Description |
|---------|-------|-------------|
| `slck init` | `--bot-token-from-env`, `--user-token-from-env`, `--bot-token-stdin`, `--overwrite`, `--no-verify` | Guided setup; stores into the keyring |
| `slck set-credential` | `--key`, `--stdin`, `--from-env`, `--ref` | Set one credential (stdin/env only) |

`config` subcommands (none accept a secret value):

| Command | Flags | Description |
|---------|-------|-------------|
| `set-token [token]` | | Set API token (auto-detects bot/user type) |
| `show` | | Show current configuration status |
| `set-token` | | Removed — errors out, points to `set-credential` |
| `show` | | Show backend, ref, which keys are present (never values) |
| `delete-token` | `--force`, `--type` | Delete stored token(s) |
| `test` | | Test authentication for configured tokens |

Expand Down Expand Up @@ -820,12 +838,16 @@ Commands have convenient aliases:

| Variable | Description |
|----------|-------------|
| `SLACK_API_TOKEN` | Bot token (overrides stored bot token) |
| `SLACK_USER_TOKEN` | User token for search (overrides stored user token) |
| `SLCK_AS_USER` | Set to `true` or `1` to default to user token instead of bot token |
| `SLACK_CHAT_API_KEYRING_BACKEND` | Force the keyring backend (e.g. `file`) — non-secret selector (§1.4) |
| `SLACK_CHAT_API_KEYRING_PASSPHRASE` | Passphrase for the encrypted-file backend (the one runtime secret-env exception, §1.4) |
| `NO_COLOR` | Disable colored output when set |
| `XDG_CONFIG_HOME` | Custom config directory (default: `~/.config`) |

> `SLACK_API_TOKEN` / `SLACK_USER_TOKEN` are **no longer read at runtime**
> (§1.11). Use `slck init --bot-token-from-env` / `slck set-credential` to
> stage a token into the keyring instead.

## Known Limitations

### Message Length Limits
Expand Down
14 changes: 12 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
module github.com/open-cli-collective/slack-chat-api

go 1.24
go 1.24.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-architecture-reviewer): cli-common is pinned at a pseudo-version (v0.0.0-20260516182733-b753d5c62d14) rather than a tagged semver release. This library owns all OS keyring I/O and defines the AllowedKeys allowlist — it is the core security component of this PR. A pseudo-version gives no semantic contract: a breaking change or security fix in the upstream library will not be flagged by go get -u tooling. The library should be tagged before this PR merges, or the go.mod should document why a tag is not yet available.

Reply to this thread when addressed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred deliberately, not blocking this PR. The pseudo-version is intentional during active Phase B credstore co-development — cli-common's API is still being shaped by each pilot CLI (slck is the first). Tagging a semver release is tracked at the epic level (INT-310) and cli-common will be pinned to a tag before Phase B closes, after the API has stabilized across gro/atlassian. Pinning to a premature tag now would force a churn of bump PRs across every CLI mid-migration.

require (
github.com/open-cli-collective/cli-common v0.0.0-20260516182733-b753d5c62d14
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.11.1
golang.org/x/term v0.27.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/99designs/keyring v1.2.2 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
golang.org/x/sys v0.28.0 // indirect
)
35 changes: 34 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
@@ -1,18 +1,51 @@
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM=
github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/open-cli-collective/cli-common v0.0.0-20260516182733-b753d5c62d14 h1:78EW5uCbAzbAO32+oY4HDaFOqS2sPYnc4AT+G5UjdL0=
github.com/open-cli-collective/cli-common v0.0.0-20260516182733-b753d5c62d14/go.mod h1:5i4MkFToMVPLBW29O01lsHS9d1m9pC0BxSOYjFDz7ds=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading
Loading