Native Shell
Native Shell: AI-generated UI, native in your app, on every platform. Apps on iOS, Android, macOS, Windows and Linux, GPUI apps and web pages load signed, sandboxed TSX modules at run time and draw th
Open-source framework for AI teammates that live in your Slack. Set up entirely from inside Claude Code. No UI, no managed service.
Developer toolsTypeScript · ShellGitHub @nitaybzOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Open-source framework for building autonomous multi-agent Slack teams on Claude Code + Max
Taken from the repository README
Loading the full README.
Read the full README ↗I run a small smart-home company called Ginnie (hence ginnie-agents). There's always more work to do than hands to do it. Investigations, reports, dashboards to scan, patterns to catch, follow-ups to chase. The work is real, and most of it is mechanical: checking, summarizing, drafting, monitoring, deciding when to escalate.
I wanted more hands. Not chatbots, not function-calling demos. Teammates: with names, with their own Slack channels, with persistent memory, with judgment, with daily rhythms. Things I can talk to in Slack like I talk to anyone else, that take ownership of an area, learn from feedback, and stop being noise.
I built ginnie-agents to make that happen, and I run my own team on it every day. This is the framework, stripped to be useful for anyone running their own.
The single most opinionated decision in this repo: Claude Code is the installer, the configurator, and the operator.
You don't run a CLI. You don't fill out a config wizard. You don't edit YAML in three places. You clone the repo, you open it in Claude Code, and you say things like:
set me up
create an agent for handling support tickets
add my co-founder to the team directory
the listener seems off, run doctor
update the framework
Every operational concern, from first install to creating an agent to pulling framework updates, is handled by a Claude Code skill that ships in this repo. The instructions, the templates, the manifest examples, the diff scripts, the helper that atomically rotates Slack config tokens, all of it is written for Claude Code to follow. You describe intent; the skills do the work.
That's why the framework feels light to set up even though it's wiring up Slack apps, Docker, PM2, git hooks, schedules, and a small daemon. The complexity is real; Claude Code absorbs it.
The Slack part can be either fully manual or roughly 80% automated, your call. During first-run setup, Claude asks whether you want to set up Slack apps programmatically or by hand. If you give it a one-time Slack workspace admin token, every future create-agent flow uses Slack's manifest API to create the app's scopes, events, and Socket Mode in a single API call. You still do three clicks per agent (install in workspace, copy bot token, generate App-Level Token) because Slack doesn't expose an API for those, but you skip the ~17 clicks of adding scopes one by one. If you'd rather keep it manual, Claude walks you through every click, top to bottom. Either way, Claude is the one telling you what to click.
You write what an agent should do; the framework handles everything else. Some real-world shapes that fit:
Pick a job, sketch the agent, ship it. Roughly 15 minutes from idea to a working teammate in your workspace, including writing the agent's voice and creating its Slack app.
If any of those don't fit, this is honest about what it isn't (see What this is NOT).
The headline isn't a single feature, it's that all of these are wired together and working out of the box:
Three tiers. The first two are always loaded into the system prompt; the third is a journal the agent greps on demand.
agents/<name>/memory/
├── rules.md <= 200 lines always loaded user-stated requirements
├── playbook.md <= 300 lines always loaded settled patterns
└── episodes/
├── 2026-Q1.md no cap grepped raw journal, append-only
├── 2026-Q2.md
└── 2026-Q3.md
rules.md is for things you say directly. "Always reply in the same language the user wrote in." "Never auto-apply changes that increase budget by more than 20%." The agent edits this file the first time you state a rule. One line, no narrative. Hard cap of 200 lines because it's in every system prompt forever.playbook.md is for patterns the agent has learned and a nightly consolidation routine has settled. "When CTR drops 20%+ for 3+ days, check device-mix shift before increasing budget." Always loaded, capped at 300 lines, only the consolidation routine writes here.episodes/ is the journal. Append-only, quarterly-rotated, NOT loaded into the system prompt. Every meaningful exchange goes here. The agent uses grep to pull exactly the past it needs when you ask "what did we conclude about X last month?"There's no "load my recent memory" tool, by design. Such a tool always loads either too much (bloated context) or too little (missed history). Letting the agent grep means it pulls exactly what's relevant to the question.
Three protections keep memory from rotting: merge=union on memory files in .gitattributes (any merge keeps both sides' lines); a commit-msg git hook that rejects commits exceeding the caps or shrinking an episode file; and the Watcher daemon that DMs you when a memory file approaches its cap, so you fix it before the hook bites.
Memory is the agent's narrative state — rules, settled patterns, the journal. For machine state — cursors into a mailbox or feed, a CRM keyed by phone, snapshots of an external system, draft caches — agents have agents/<name>/data/. The framework mounts it RW at /workspace/data inside the container, with no schema and no naming convention. Optional: agents that don't need it just skip the directory.
Why a separate concept from memory? Different shapes, different lifecycles. Memory is text the agent reads to think; data is JSON the agent reads to resume. Keeping them apart means memory caps and merge=union don't accidentally apply to JSON files (which would corrupt them on conflict), and data/ doesn't need any of the per-line discipline memory enforces.
Each agent has a schedules.json it owns. Cron-style entries the listener watches and reloads automatically:
{
"schedules": [
{ "id": "morning-brief", "cron": "0 8 * * *", "message": "Morning brief: top 3 issues, anything new overnight." },
{ "id": "fleet-scan", "cron": "*/30 8-21 * * 1-5", "message": "Scan the fleet. Open tasks for new issues. Stay silent if nothing changed." },
{ "id": "memory-consolidate","cron": "30 4 * * *", "message": "Run the consolidation routine.", "enabled": true }
]
}
Agents can edit this file themselves at runtime. "Add a weekly review at Mondays 10am" and the agent does it. The listener reloads on file change, no restart needed.
Every message reaching an agent is prefixed by the framework with sender identity:
From: <name> | role: <role> | email: <if known>
@mention text from the user
Agents respond differently to a founder, a peer agent, a teammate, or an unknown user. Sender identity comes from the merged team directory; if a sender isn't in the directory, the framework marks them unknown or external. For agents with boundaries: "write", the listener refuses to dispatch from unknown/external senders at all — the agent never wakes up. Defense-in-depth on top of any prompt-level guidance. Read-only agents are not gated (their blast radius is bounded). Per-agent opt-out: "allow_unverified_senders": true in config.json, for cases like a public Q&A bot where you do want random Slack users answered.
Slack voice memos and audio attachments are transcribed by whisper.cpp on your host before the agent ever sees them. The agent receives the transcript spliced inline as the message text and never sees the audio file. Fully offline, zero per-message cost.
[Voice message, transcribed locally — original: "audio_message.webm" (audio/webm)]
hey can you check what's happening with the deploy this morning
Opt-in at setup. The first run of /setup asks Do you want to enable voice-message transcription? [Y/n] and runs bash scripts/install-whisper.sh if you say yes — clones whisper.cpp, builds it, downloads the multilingual small model (~466 MB). One-time. Skipping is fine; without it, audio attachments fall back to standard download stubs (the agent can't read those) and the listener logs a one-time warning. Requires ffmpeg and a C++ toolchain (cmake/make, g++) on the host. The point: same conversational experience whether your team types or speaks, with no per-minute speech-to-text bill.
Each agent has work_hours in its config.json, but it does not gate inbound messages: an @mention, DM, or thread reply gets answered at any hour, any day. There is no setting that makes an agent go quiet on inbound messages outside a window.
{
"work_hours": {
"enabled": true,
"start": "09:00",
"end": "18:00",
"days": ["mon", "tue", "wed", "thu", "fri"]
}
}
work_hours is a record of the agent's intended availability, nothing more. What actually keeps an agent from proactively starting a conversation outside its hours is each routine's own cron expression in schedules.json — a routine fires exactly when its cron says, regardless of work_hours. If you want an agent quiet outside certain hours, constrain the routine's schedule; there's currently no way to silence inbound replies.
Skills are reusable knowledge or procedure. Three layers, all loaded automatically:
framework/skills/ shipped with the framework, every agent gets them (memory-curation, etc.)
shared/skills/ your cross-agent skills (a CRM client, a SQL templates pack, anything)
agents/<name>/skills/ personal to one agent
Write a skill once in shared/skills/, every agent on your team picks it up. No duplication, no drift, edit one place. Two different agents that both touch the same CRM share the same client skill instead of each having their own.
Your team directory lives in two places, merged at runtime:
shared/known-users.json for people most agents should know.agents/<name>/known-users.json for people only this agent knows (a customer-success agent's customers, a vendor list visible only to ops).Same key in both, the local entry wins for that key only. Otherwise union. This means selective visibility: a customer-success agent can know specific customers without those entries leaking into the operations agent's team directory. When you create a new agent, the manage-known-users skill asks one question: "Visible to all agents, specific agents, or none?" and only follows up if you said specific.
Each agent's config.json declares it as "boundaries": "read-only" or "write". The runner enforces this by overriding allowed_tools at spawn time:
read-only restricts the toolkit to Read, Grep, Glob, WebSearch, WebFetch. No Bash, no Write, no Edit. Enforced at the SDK layer regardless of what the prompt says. Note: this prevents the agent from mutating state (writing files, running shell commands), but it does not prevent outbound data exfiltration (a read-only agent can still Read secrets and WebFetch them out). Use read-only to contain blast radius from misbehavior, not as a guarantee against a compromised agent leaking data; the threat-model section in ARCHITECTURE.md is explicit about what each layer does and does not protect against.write is the default; agent gets the full toolkit it declared.Useful for analyst agents that should never accidentally mutate state, or for any agent you're not yet ready to give shell access to.
Each agent has a SOUL.md: a short backstory, voice rules, and a couple of quirks. The runner injects it into the system prompt between the team directory and the operational layer, so the agent forms identity before learning its job. Optional, but the difference between a "polished robot" and "actually feels like a person to talk to" is small (25 to 35 lines) and worth it.
Each agent also gets its own Slack avatar, picked by you. The create-agent skill derives an image-generation prompt from SOUL.md (so the picture matches the personality), tells you to run it through whatever image AI you prefer, and ships an ImageMagick one-liner that crops the result to Slack's 1024 x 1024 square. Optional, but agents with real avatars feel a lot more like teammates than agents with the default Slack bot icon.
This is the part that makes self-hosting safe to actually use:
flowchart LR
Host["Your host machine<br/>(Claude Code, your global<br/>plugins, your skills, your<br/>shell, your secrets)"]
Listener["listener (Node, PM2)<br/>auto-discovers agents,<br/>spawns containers"]
A["Agent A container<br/>fresh, ephemeral,<br/>read-only mounts only"]
B["Agent B container<br/>fresh, ephemeral,<br/>read-only mounts only"]
C["Agent C container<br/>fresh, ephemeral,<br/>read-only mounts only"]
Host --> Listener
Listener --> A
Listener --> B
Listener --> C
Every agent session runs in its own fresh Docker container via the Claude Agent SDK. The container has:
PROMPT.md, SOUL.md, memory, skills, schedules (mounted read-only or read-write per file).shared/skills/ and shared/known-users.json (read-only).framework/skills/memory-curation/SKILL.md (read-only).That's it. The container does NOT have:
/workspace/ and /home/node/.claude/ inside the container.Containers are short-lived (one per session), stateless beyond what they write to mounted memory files, and isolated from each other. This means you can run a marketing agent that touches your ad accounts and an ops agent that touches your infrastructure on the same host with confidence: neither can see the other's credentials, mess with the other's memory, or accidentally invoke the other's skills.
It also means agents stay clean. Your host's globally-installed Claude Code plugins, skills, MCP servers, and personal preferences don't bleed into how an agent behaves. What you wrote in PROMPT.md and SOUL.md is what the agent is, every time.
The framework supports two ways to authenticate the Claude Agent SDK calls each container makes. Pick the one that matches how you run the platform.
Generated by claude setup-token during first-run setup. Long-lived (~1 year), cheap (flat subscription cost), zero per-call billing. This is what the setup skill configures by default.
Read this before you choose it. Per Anthropic's usage policy, OAuth authentication is intended for ordinary, individual use of Claude Code by the subscriber. Anthropic explicitly does not permit "routing requests through Free, Pro, or Max plan credentials on behalf of [other] users."
In practice, OAuth is appropriate when:
It is not appropriate (and risks token revocation or account suspension) when:
Anthropic reserves the right to enforce these restrictions without prior notice. If your use case looks anything like the second list, use Option B.
Generated at console.anthropic.com. Billed per-token. Fully supported by Anthropic's terms for automation, products, and multi-user scenarios. Higher cost than a flat subscription, but no authentication risk.
Set either CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY in .env. If both are set, ANTHROPIC_API_KEY wins. The setup skill asks which path you want during first-run setup.
Everything runs through Slack's Socket Mode (WebSocket transport). No public URLs, no webhooks, no reverse proxies, no static IPs. Works behind NAT, on a laptop, on a home server, anywhere with outbound TCP.
Each agent has its own Slack app, with its own avatar, its own bot identity, its own channel. The listener spawns one Bolt app per agent, so message routing is handled natively by Slack: an @mention or DM on agent A's app reaches only agent A's handlers.
sequenceDiagram
autonumber
participant U as User in Slack
participant L as Listener (host, persistent)
participant C as Agent container (fresh)
U->>L: @mention or DM
L->>L: Resolve sender identity from team directory
L->>C: Spawn container with mounts
Note over C: SOUL + PROMPT + memory<br/>+ skills + team dir<br/>composed into system prompt
C->>C: Reason. Possibly grep episodes.<br/>Write to memory.
C->>U: chat.postMessage reply (in agent's voice)
C->>L: Persist session id for thread continuity
Note over L: Container exits.<br/>Memory + sessions on host.
The repo ships with nine Claude Code skills that cover the entire lifecycle. You never run a CLI; you ask Claude.
| Skill | What it does | Trigger phrase examples |
|---|---|---|
| setup | First-run setup. Verifies prerequisites, runs claude setup-token for the long-lived token, scaffolds .env, installs git hooks, builds the Docker image, builds the listener, starts PM2. | "set me up", "first run", "install" |
| create-agent | Walks you through creating a new agent end to end. Discovery, SOUL, mission, schedule, boundaries, work hours, avatar prep, Slack app creation (programmatic via manifest API or fully manual), team directory registration, smoke test. | "create an agent for handling support tickets", "new agent" |
| update-framework | git pull from upstream, conditional Docker rebuild, conditional listener rebuild, pm2 restart, doctor verification. Also runnable as a script for the Watcher's [Update now] button. | "update the framework", "upgrade" |
| doctor | Health check across prerequisites, env, hooks, listener, Docker image, agents, memory caps, disk. Delegates to scripts/doctor.sh for deterministic mechanical checks; the skill adds Slack-reachability and token-age context. | "doctor", "is everything ok" |
| manage-known-users | Add, edit, or remove humans and agents from the team directory. Asks the visibility tree question (all agents, specific agents, or none) so you don't accidentally make every entry visible to every agent. | "add a known user", "register my new teammate" |
| manage-routines | View, add, edit, or disable schedules in an agent's schedules.json. Live-reloaded by the listener; no restart needed. | "add a routine for the ops agent", "change the schedule" |
| manage-work-hours | Record an agent's intended availability window in config.json for reference. Does not gate inbound replies — points you to manage-routines for actually limiting when an agent acts. | "make the sales agent only respond during business hours" |
| logs | Tail, search, or download listener and per-agent logs. Wraps and per-agent log files with friendly diagnostics for common symptoms. |
Plus an atomic helper at scripts/rotate-slack-config-token.sh that rotates Slack config tokens and writes the new pair back atomically, so that "I rotated and lost the new pair" failure mode can't happen.
The framework ships one bot, called Watcher. Not an AI agent (no Claude tokens). It runs alongside the listener, watches your install, and DMs you on Slack only when something needs attention. Interactive buttons on actionable alerts:
[Update now] [Remind tomorrow] [Skip this version]. Click [Update now] and Watcher edits the message in place. Updating... then the tail of the actual git pull and rebuild output, then Updated.[View logs] [Restart listener]. Last 30 lines posted in the same DM, restart actionable from Slack.[Ack 24h] [Ack 7d].Plus a /watcher slash command in any channel: status, check, pause [hours], resume, doctor.
Watching df and git fetch doesn't need AI; Watcher is plain Node + Bolt + shell-out, deterministic, free, fast. Agents get their own model (judgment work, AI, container per session). The framework treats them as different shapes on purpose.
You need:
git clone https://github.com/nitaybz/ginnie-agents
cd ginnie-agents
Open the directory in Claude Code and ask:
"set me up"
The setup skill handles the rest. End state: a running listener with no agents.
Then:
"create an agent for <role>"
That's it.
The agents you create under agents/ are not sample data. Each one holds real files describing real people and real business context: who they report to, what they're allowed to do, private notes the agent has learned over time. That's on purpose, by design, for the framework: it is deliberately built so a private fork can commit and version all of that (so the agent's memory survives across machines and doesn't get lost). The project's tracked .gitignore does not hide agents/, and it never should.
That default becomes a trap the moment your fork of this repo is public. A public fork gives strangers read access to your git history, so if any of that private content ever gets committed, it's exposed for good.
If your fork is public, run this once on every machine where you clone it:
bash scripts/private-fork-setup.sh
What it does:
agents/<name>/), your optional company context (shared/foundation.md), your local docs/, and a couple of other private-only paths from git status and git add, so you can't commit them by accident.shared/known-users.json (your real team directory — names, emails, roles) so git ignores changes to it entirely; the version tracked by the repo stays an empty template.origin remote looks public.These protections live only in your local .git/ folder. They are not part of the repository and do not travel with git clone, git push, or git pull — that's why the script has to be run again on every new machine or fresh clone.
Setting expectations honestly:
If any of those are dealbreakers, this isn't your framework.
data/ directory and Block Kit hygiene rules). Other notable milestones along the way: v0.2.0 (the Watcher), v0.2.2 (security hardening from a community audit), v0.2.4 (ANTHROPIC_API_KEY as a first-class auth path), v0.2.5 (file uploads end-to-end), v0.2.6 (routines schema auto-injected so agents stop silently writing the wrong shape), v0.2.7 (local voice-message transcription via whisper.cpp). See Releases and CHANGELOG for the full history.If you try it and something breaks, file an issue. PRs welcome, especially on the skills.
MIT.
pm2 logs| "show logs", "why didn't the agent respond at 3pm?" |
| setup-watcher | Wire up the optional Watcher daemon. Creates the Watcher's Slack app via manifest, walks the install, captures tokens, configures .env, registers the PM2 process. | "set up the watcher", "watchdog" |