Local Slack Web to Codex bridge for Bob.
Project description
personal-slack-agent
Setup | How It Works | Commands | Configuration | Publishing | Latest GitHub Release
personal-slack-agent is a local Slack-to-Codex bridge. It runs a background
agent named Bob on your machine, watches approved Slack conversations, starts
or resumes local Codex sessions, and posts status plus results back into Slack
threads.
Why Bob Exists
Some company environments restrict direct integrations between AI coding tools and internal messaging systems. A team may not be able to install a Slack app, grant OAuth scopes to a hosted connector, expose internal messages to a third party service, or run a cloud bot that talks to Codex on the user's behalf.
Bob is built for that constraint. Instead of acting as a hosted Slack app, Bob runs locally beside Codex and uses a browser-authenticated Slack Web session that the user already controls. Work still happens on the user's machine, while Slack becomes the coordination surface: prompts, progress, waiting states, approvals, and final answers remain visible in the approved company messaging system.
The goal is not to replace Codex. The goal is to make Codex work trackable from Slack when Slack is where the team already coordinates.
What Bob Does
- Watches configured Slack channels or explicitly allowed runtime conversations.
- Accepts messages that invoke a configured Slack callsign such as
Boborbob. - Starts a new local Codex session for a new Slack thread.
- Resumes the same local Codex session when someone replies in that thread.
- Posts working, waiting, approval, error, and final messages back to Slack.
- Lets terminal-originated requests use the same Slack-thread-backed workflow.
- Keeps per-channel memory policy explicit so shared channels do not update personal durable notes by accident.
How It Works
At a high level:
- Bob attaches to a local Chrome session that is already logged into Slack.
- Bob watches Slack Web realtime events and targeted Slack Web API responses.
- When an allowed user invokes Bob, Bob creates or resumes local Codex work.
- Bob posts lifecycle updates and final output back into the Slack thread.
The Slack thread is the human-readable work log. The local state database maps Slack threads to Codex session ids so later Slack replies can continue the same conversation.
For a deeper architecture walkthrough, see docs/how-it-works.md.
Current Status
The project is functional but still experimental.
Working pieces include:
- package install and CLI entry points
- config generation and validation
- background watcher loop
- websocket-first Slack event detection
- targeted Slack API hydration for channel roots and thread replies
- thread/session mapping to local Codex sessions
- thread reply resume for existing sessions
- waiting-state reminders and auto-close handling
- manual
<callsign> closethread closure with later resume support - cleanup of obsolete waiting prompts after resolution
- local process control with
bobctl start|stop|restart|status|tail-log|show-config|doctor - GitHub Actions CI, generated GitHub Releases, manual TestPyPI publishing, and PyPI publishing from generated release artifacts
Current constraints:
- macOS only
- Chrome or Chromium required
- Slack integration uses Slack Web realtime sockets plus browser-session-backed Slack Web requests, not an official Slack app install
- broad Slack message edit/delete flows are still limited beyond targeted waiting-prompt cleanup
- browser/web request behavior may need adjustment if Slack changes private web client APIs or websocket behavior
Quick Start
For a full first-time setup, use docs/setup.md.
Local Development
Use an editable install when working from a repo checkout:
python3 -m venv .venv
.venv/bin/python -m pip install -U pip
.venv/bin/python -m pip install -e '.[dev]'
TestPyPI Install
Use TestPyPI only for release testing. Install in a throwaway virtual environment so the package does not conflict with an editable local Bob install:
python3 -m venv /tmp/bob-testpypi
/tmp/bob-testpypi/bin/python -m pip install --upgrade pip
/tmp/bob-testpypi/bin/python -m pip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
personal-slack-agent
--extra-index-url lets pip resolve normal dependencies such as Playwright from
PyPI when they are not available on TestPyPI.
PyPI Install
After the project has a real public PyPI release, install from PyPI with:
python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install personal-slack-agent
Generate local config:
.venv/bin/bob-init
The wizard prompts for the owner identity, Slack workspace URL, channel, default working directory, and channel memory policy, then writes a validated config:
~/.config/personal-slack-agent/bob.toml
Also included in the repo:
config/bob.sample.toml
docs/setup.md
docs/bob-config-setup.md
docs/command-reference.md
Example config
[defaults]
owner_name = "Bob Owner"
owner_preferred_name = "Owner"
# Optional Slack callsigns. Empty or omitted falls back to ["Bob"].
assistant_names = ["Bob"]
[browser]
slack_signin_url = "https://slack.com/signin?entry_point=nav_menu#/signin"
browser_mode = "shared_browser"
browser_url = "http://127.0.0.1:9222"
cdp_url = "http://127.0.0.1:9222"
browser_user_data_dir = "/Users/you/.cache/personal-slack-agent/chrome-profile"
[runner]
bob_codex_home = "/Users/you/.local/share/personal-slack-agent/codex-home"
codex_exec_timeout_seconds = 600
[lifecycle]
reminder_minutes = [30]
auto_close_minutes = 120
[orchestrator]
max_concurrent_tasks = 1
max_concurrent_per_thread = 1
[watcher]
root_batch_size = 50
thread_batch_size = 200
thread_reply_rate_limit_backoff_seconds = 60
recent_terminal_thread_reconcile_limit = 6
periodic_terminal_thread_reconcile_batch_size = 1
historical_terminal_thread_reconcile_base_interval_seconds = 60
historical_terminal_thread_reconcile_max_interval_seconds = 900
bob_ultimate_mode = false
[[workspaces]]
name = "my-workspace"
slack_url = "https://app.slack.com/client/T12345678/C12345678"
slack_api_origin = "https://example.enterprise.slack.com"
slack_api_token = "xoxc-..."
[workspaces.channel_defaults]
allowed_actor_ids = ["U12345678"]
default_cwd = "/Users/you/Code"
additional_roots = ["/Users/you/Code"]
accept_root_bob_requests = true
codex_home_mode = "default"
[[workspaces.channels]]
name = "my-private-channel"
allowed_actor_ids = ["U12345678"]
persistent_memory_mode = "owner_only"
persistent_memory_owner = "bob_owner_handle"
[[workspaces.channels]]
name = "my-shared-bob-channel"
codex_sandbox_mode = "workspace-write"
persistent_memory_mode = "disabled"
watcher.bob_ultimate_mode = false preserves the current configured-channel Bob behavior. Set watcher.bob_ultimate_mode = true to allow explicit configured-callsign invocation from any accessible public/private channel, DM, or group DM, still restricted by allowed_actor_ids; in that mode Bob appends working and final status into the invoking message instead of posting a separate Bob reply for that invocation.
defaults.assistant_names controls Slack-facing callsigns. If omitted or set to an empty list, Bob uses the legacy callsign Bob. You can configure aliases such as ["Bob", "Bobby", "Copilot"]; matching is case-insensitive with a name boundary, and Bob replies using the configured spelling of the matched alias.
The operational command names are fixed and do not change with Slack callsigns: bob, bobctl, bob-agent, and bob-init remain the commands.
Important config notes
For a field-by-field explanation of bob.toml, see:
docs/bob-config-setup.md
-
allowed_actor_idsAt[workspaces.channel_defaults], this defines who may trigger or resume Bob work in that workspace's channels by default. Optionally set it again on[[workspaces.channels]]to override that workspace default for one channel. -
owner_name/owner_preferred_nameAt[defaults], these define how Bob refers to the human owner in runtime prompts. For committed examples, keep these anonymized. -
assistant_namesAt[defaults], this defines the Slack callsigns that can invoke Bob. Bob replies using the exact alias the user typed for that interaction. If omitted or empty, the default is["Bob"]. -
workspaces.channel_defaultsUse this to define the default cwd, roots, Bob acceptance policy, and Codex sandbox/home behavior that should apply to channels in one workspace unless a channel overrides them directly. -
slack_urlThis should point to any Slack client URL inside the target workspace. Bob resolves per-channel ids from the rendered sidebar DOM at startup, so channels only need names in config. -
slack_api_originThis is the same-origin Slack web host Bob will use for browser-session-backed/api/...calls. -
slack_api_tokenThis is currently the browser-session token used for the private Slack web API path. Treat it as sensitive. -
post_terminal_threads_hereChannels with this flag can be targeted by thebobterminal wrapper for terminal-originated Bob requests. If exactly one channel across your config has this flag,bob "<prompt>"can use it by default. -
persistent_memory_modeRequired for every configured channel. Useowner_onlyfor a private channel that is allowed to update one person's durable preference notes. Usedisabledfor shared or test channels that must not update personal durable notes. -
persistent_memory_ownerRequired only whenpersistent_memory_mode = "owner_only". This identifies whose durable preference notes the channel may update. -
slack_channel_idOptional per channel. Use this when Slack does not expose the channel in the rendered sidebar for Bob's browser session. If provided, Bob will seed the channel route directly instead of relying on sidebar discovery for that channel.
Automatic Slack auth bootstrap
If the target workspace is already open in your debuggable Chrome session, you can populate
slack_api_origin and slack_api_token automatically:
.venv/bin/bob-init --discover-slack-auth --workspace my-workspace
browser_modeSupported values:shared_browserdedicated_browser
Chrome setup
Bob expects a debuggable Chrome session.
Start Chrome with remote debugging enabled:
open -na "Google Chrome" --args \
--remote-debugging-port=9222 \
--user-data-dir="$HOME/.cache/personal-slack-agent/chrome-profile" \
--no-first-run \
--no-default-browser-check \
"https://slack.com/signin?entry_point=nav_menu#/signin"
In that Chrome instance:
- open
chrome://inspect/#remote-debugging - enable remote debugging
- log into Slack
- open any page inside the workspace you configured
Bob Chrome Dock Launcher
Install the reusable debug-browser app:
.venv/bin/bobctl install-chrome-launcher --force
This writes ~/Applications/Bob Chrome.app.
At install time, Bob renders that launcher from the current [browser] config in bob.toml:
browser.cdp_urlbrowser.browser_user_data_dirbrowser.chrome_executable_pathwhen set
If you use a non-default config file, pass it when installing:
.venv/bin/bobctl install-chrome-launcher --config ~/.config/personal-slack-agent/bob.toml --force
Behavior:
- if the configured
browser.cdp_urlis already live, it just foregrounds the configured Chrome app - otherwise it launches a fresh debug-enabled Chrome instance using the configured app/profile settings
- it does not open Slack or any other URL automatically
Bob Chrome.app is safe to pin to the macOS Dock.
If you later change the [browser] settings, rerun the same install command to refresh the installed app, including --config ... when you use a non-default config file.
This launcher is for the browser only. Bob itself still must be started or restarted from a normal unsandboxed shell.
Usage
For a command-by-command operator reference, see docs/command-reference.md.
Start Bob in background
.venv/bin/bobctl start --config ~/.config/personal-slack-agent/bob.toml --poll-interval-seconds 10
Run a live smoke test:
.venv/bin/bobctl smoke-test --workspace my-workspace --channel my-private-channel
Restart Bob:
.venv/bin/bobctl restart --config ~/.config/personal-slack-agent/bob.toml --poll-interval-seconds 10
Tail logs:
.venv/bin/bobctl tail-log --lines 50
Stop Bob:
.venv/bin/bobctl stop
One-shot cycle
For debugging:
.venv/bin/bob-agent --once --config ~/.config/personal-slack-agent/bob.toml
Triggering work from Slack
Send a message in a watched channel that starts with a configured callsign, for example:
Bob, summarize this repo
Bob will:
- create or use the Slack thread for that request
- start a local Codex session
- post:
_*Bob is working on it :arrows_counterclockwise::*_ <session-id>
- post final output as:
_*Bob :white_check_mark::*_ ...
If you reply in the thread later, Bob resumes the same local Codex session.
If Bob is waiting for input or approval:
- configured reminders apply only to those waiting states
- auto-close applies only to those waiting states
- reply with
<callsign> closeto close the thread without losing the underlying Codex session - reply again later in the same thread to resume
Terminal Requests
You can start a Bob request from the terminal with the bob wrapper:
.venv/bin/bob --workspace my-workspace --channel my-private-channel "summarize this repo"
Docs
- Setup guide: install, Chrome setup, first config, smoke test.
- How it works: motivation, architecture, and message flow.
- Config guide: field-by-field
bob.tomlreference. - Command reference:
bob,bob-agent,bob-init, andbobctl. - Publishing guide: GitHub Releases, TestPyPI, PyPI options, and package exposure.
- Slack client findings: implementation notes from Slack Web inspection.
- Sample config: committed anonymized config template.
Release And Publishing
GitHub Releases are generated automatically from successful pushes to main.
Each generated release uploads a wheel and source distribution as downloadable
release assets.
TestPyPI publishing is configured but manual. PyPI publishing is wired to run
after each successful generated GitHub Release once the PyPI Trusted Publisher
and GitHub pypi environment are configured. The PyPI workflow downloads the
exact wheel and source distribution from the GitHub Release, so the PyPI package
version matches the release tag. The same workflow can also publish an existing
GitHub Release tag manually as a fallback.
See docs/publishing.md for the setup values and release workflow details.
The CI badge above updates when GitHub renders the README. The latest-release link resolves through GitHub to the current latest release. Literal version numbers in README prose only change when a commit changes them.
Security Notes
slack_api_tokenis sensitive and should not be committed.- Do not publish personal
bob.tomlfiles. - Do not share the Chrome profile used for Slack browser auth.
- Treat GitHub Releases, TestPyPI, and PyPI as public distribution channels.
- Published Python wheels contain readable
.pysource files.
Project Layout
src/personal_slack_agent/: package sourcetests/: automated testsconfig/bob.sample.toml: anonymized sample configdocs/: setup, operation, architecture, and publishing docs.github/workflows/: CI, release, TestPyPI, and PyPI workflowspyproject.toml: package metadata
Testing
Run the full test suite:
.venv/bin/python -m pytest -q
License
This project is licensed under the MIT License. See LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file personal_slack_agent-0.1.13.tar.gz.
File metadata
- Download URL: personal_slack_agent-0.1.13.tar.gz
- Upload date:
- Size: 124.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fd38943528e0837ec9cc2bb80d3cf82fea3bab8e5e6b68a17690090e705a7084
|
|
| MD5 |
0b00f2e0a2c791c7b8adae298e7f64d7
|
|
| BLAKE2b-256 |
2713ece8b1b1813311f8e53cdf278d13d7e1203799f2c35cf4d3bced8afdcf57
|
Provenance
The following attestation bundles were made for personal_slack_agent-0.1.13.tar.gz:
Publisher:
pypi.yml on ethanyc216/personal-slack-agent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
personal_slack_agent-0.1.13.tar.gz -
Subject digest:
fd38943528e0837ec9cc2bb80d3cf82fea3bab8e5e6b68a17690090e705a7084 - Sigstore transparency entry: 1429936441
- Sigstore integration time:
-
Permalink:
ethanyc216/personal-slack-agent@c2d5b4ff2de8a8153e739ccb5450dfe0d29271de -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ethanyc216
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@c2d5b4ff2de8a8153e739ccb5450dfe0d29271de -
Trigger Event:
workflow_run
-
Statement type:
File details
Details for the file personal_slack_agent-0.1.13-py3-none-any.whl.
File metadata
- Download URL: personal_slack_agent-0.1.13-py3-none-any.whl
- Upload date:
- Size: 80.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72a5bb21e7122fc74d56046111b0297584fea457f74371ae57e320b9d318b019
|
|
| MD5 |
ec20a56011b3479e69915c9c5ab3dbf9
|
|
| BLAKE2b-256 |
b243e9031ce962f408e74cf4b2e17ea8606d86b1647705126c6236d3fddd6c27
|
Provenance
The following attestation bundles were made for personal_slack_agent-0.1.13-py3-none-any.whl:
Publisher:
pypi.yml on ethanyc216/personal-slack-agent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
personal_slack_agent-0.1.13-py3-none-any.whl -
Subject digest:
72a5bb21e7122fc74d56046111b0297584fea457f74371ae57e320b9d318b019 - Sigstore transparency entry: 1429936448
- Sigstore integration time:
-
Permalink:
ethanyc216/personal-slack-agent@c2d5b4ff2de8a8153e739ccb5450dfe0d29271de -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ethanyc216
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@c2d5b4ff2de8a8153e739ccb5450dfe0d29271de -
Trigger Event:
workflow_run
-
Statement type: