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
51 changes: 50 additions & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "yourconscience",
"description": "Self-hosted marketplace for the dotagents cross-agent skill library.",
"description": "Self-hosted marketplace for the dotagents cross-agent skill library: the whole repo as one plugin, plus portable skills as single-skill plugins.",
"owner": {
"name": "Kirill Korikov",
"email": "korikov.kirill@proton.me"
Expand All @@ -10,6 +10,55 @@
"name": "dotagents",
"source": "./",
"description": "Cross-agent skill library and subagent roles from the dotagents repo."
},
{
"name": "tech-search",
"source": "./skills/tech-search",
"description": "Search Hacker News, X.com, Reddit, Discord, and high-signal tech blogs for curated opinions on a topic.",
"strict": false,
"category": "research"
},
{
"name": "grill-me",
"source": "./skills/grill-me",
"description": "Pressure-test a plan one question at a time until scope, dependencies, and open questions are concrete.",
"strict": false,
"category": "planning"
},
{
"name": "humanizer",
"source": "./skills/humanizer",
"description": "Final-pass rewriting for concise writing that keeps the user's voice and removes generic AI tone.",
"strict": false,
"category": "writing"
},
{
"name": "repo-eval",
"source": "./skills/repo-eval",
"description": "Find, triage, and deep-evaluate GitHub repos for a given need before adopting one.",
"strict": false,
"category": "research"
},
{
"name": "spec",
"source": "./skills/spec",
"description": "Produce a minimal SPEC.md through a short interview and use it as the source of truth for complex work.",
"strict": false,
"category": "planning"
},
{
"name": "pr-triage",
"source": "./skills/pr-triage",
"description": "Inspect PR failed checks and unresolved review comments, fix valid feedback, and drive a single fix-commit-push loop.",
"strict": false,
"category": "workflow"
},
{
"name": "tmux",
"source": "./skills/tmux",
"description": "Generic tmux reference for sessions, windows, panes, screen capture, and input.",
"strict": false,
"category": "workflow"
}
]
}
29 changes: 29 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Release

on:
push:
tags:
- "v*"

permissions:
contents: write

jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-go@v5
with:
go-version: "1.24"

- uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: "~> v2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
.DS_Store
tmp/

# Personal config overlay (merged over dotagents.yaml, never committed)
dotagents.local.yaml

# Local agent runtime state
external/
.claude/
Expand Down
34 changes: 34 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
version: 2

project_name: dotagents

builds:
- id: dotagents
dir: cmd/dotagents
main: .
binary: dotagents
env:
- CGO_ENABLED=0
goos:
- darwin
- linux
goarch:
- amd64
- arm64
ldflags:
- -s -w

archives:
- id: default
formats: [tar.gz]
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"

checksum:
name_template: checksums.txt

changelog:
use: github
filters:
exclude:
- "^docs:"
- "^test:"
60 changes: 57 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ This repo is the canonical `~/.agents` layer. It detects installed agent platfor

[`AGENTS.md`](./AGENTS.md) is the canonical instruction file. [`CLAUDE.md`](./CLAUDE.md) is only a compatibility shim for agents that look for Claude-style project memory.

## CLI
## Install

Install the repo-owned CLI:
Prebuilt binaries for macOS and Linux (amd64/arm64) are attached to [GitHub Releases](https://github.com/yourconscience/dotagents/releases). With Go installed:

```bash
go install github.com/yourconscience/dotagents/cmd/dotagents@latest
```

Or from a clone:

```bash
go install ./cmd/dotagents
Expand All @@ -29,6 +35,27 @@ dotagents status
dotagents deps check
```

Releases are cut by pushing a `v*` tag; CI runs GoReleaser, which builds the archives and publishes the GitHub Release.

## Alternatives

How dotagents compares to other cross-agent config sync tools:

| | dotagents | [skillshare](https://github.com/runkids/skillshare) | [vsync](https://github.com/nicepkg/vsync) | [agents-cli](https://github.com/amtiYo/agents) |
|---|---|---|---|---|
| Skills sync | yes (symlinks + config-driven dirs) | yes | yes | yes |
| MCP sync | yes | no | yes | yes |
| Hooks sync | yes (Claude Code, Codex, Hermes, Droid) | no | no | no |
| Native subagent roles | yes (Claude Code, Codex, Droid) | agents as files | yes | no |
| Plugin catalog | yes (first-party `dotagents.yaml` entries) | no | no | no |
| External skill pinning | yes (`dotagents.lock`) | version tracking | no | no |
| Skill security audit | yes (`dotagents doctor`) | yes | no | no |
| Local private overlay | yes (`dotagents.local.yaml`) | no | no | no |
| Target agents | Claude Code, Codex, Amp, Hermes, Factory Droid, Pi/OpenClaw | Claude Code, Codex, Cursor, Gemini, 60+ | Claude Code, Cursor, OpenCode, Codex | Codex, Claude Code, Gemini CLI, Cursor, Copilot, others |
| Language | Go | Go | TypeScript | TypeScript |

dotagents focuses on the post-IDE agent stack (Hermes, Amp, Droid, OpenClaw/Pi alongside Claude Code and Codex) and on syncing the full surface - skills, MCP, hooks, roles, plugins, root instructions - from one canonical `~/.agents` layer.

## Agents

Reusable agent role definitions for agent-native subagents. Canonical roles live in `agents/*.yaml`; `dotagents sync` renders them to each configured native format:
Expand Down Expand Up @@ -60,10 +87,30 @@ Reference these from TeamCreate teammates, Claude Code subagent types, or Codex
- `spawn` - spawn and manage Claude Code agent teams with model routing and cmux integration.
- `tg` - read Telegram chats, search messages, and list dialogs through the read-only `tg` CLI.
- `tech-search` - gather high-signal opinions from tech communities and blogs on a topic.
- `tg` - read Telegram chats, search messages, and list dialogs via the read-only `tg` CLI.
- `x-cli` - unofficial CLI for `x` tooling.
- `x-sim` - offline X audience simulation for draft tweets and handle positioning.

## Installing these skills without dotagents

The repo doubles as a [Claude Code plugin marketplace](https://code.claude.com/docs/en/discover-plugins): `.claude-plugin/marketplace.json` exposes the portable skills (`tech-search`, `grill-me`, `humanizer`, `repo-eval`, `spec`, `pr-triage`, `tmux`) as single-skill plugins.

```text
/plugin marketplace add yourconscience/dotagents
/plugin install tech-search@yourconscience
```

For any agent managed by dotagents, consume the same skills as an external source with a `skills` allowlist:

```yaml
external_skills:
- url: https://github.com/yourconscience/dotagents
skill_dir: skills
branch: main
skills: [tech-search, grill-me, humanizer, repo-eval, spec, pr-triage, tmux]
```

Other sync tools that install skills from a git repo (e.g. skillshare) can point at the `skills/` directory directly.

## External Skills

Skills from external git repos can be synced alongside local skills. Declare sources in `dotagents.yaml` when needed:
Expand All @@ -73,10 +120,17 @@ external_skills:
- url: https://github.com/example/shared-skills
skill_dir: skill
branch: main
skills: [alpha, beta] # optional allowlist; omit to take every skill
```

`dotagents sync` clones or updates each repo into `~/.agents/external/<repo-name>/` and symlinks discovered skills into agent skill roots. `dotagents status` shows external sources with their commit hash. `dotagents doctor` validates that clones exist and contain valid skills.

External sources are pinned in `dotagents.lock` (commit this file): the first sync records each source's commit, and later syncs keep the source at the pinned commit instead of silently tracking the branch. `dotagents external list` shows pin state; `dotagents external update [name ...]` moves sources to the latest branch head and rewrites the lock. `dotagents doctor` warns when a source is unpinned or its cache drifts from the lock, and runs a content audit over external skills that flags risky patterns (pipe-to-shell installs, base64-decode-to-shell, prompt-injection phrasing, credential paths) for human review.

## Local overlay

`dotagents.local.yaml` next to `dotagents.yaml` (gitignored) holds personal additions that should stay out of public git: extra agents, external skill sources, MCP servers, hooks, or plugin entries. Entries merge by name (external sources by repo name); a matching name replaces the public entry wholesale, everything else is appended.

## Plugins

Dotagents treats third-party plugins as first-party catalog entries in `dotagents.yaml`, not as committed `.codex-plugin`, `.amp/`, or `.hermes/` runtime directories. (The repo's own `.claude-plugin/` manifests are the one exception; see "Installing this repo as a Claude Code plugin" below.) A plugin entry records its source format, runtime surfaces, target agents, and review notes:
Expand Down
136 changes: 136 additions & 0 deletions cmd/dotagents/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package main

import (
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)

// skillAuditPatterns flags supply-chain and prompt-injection risks in skill
// content. Matches are warnings for human review, not verdicts.
var skillAuditPatterns = []struct {
name string
re *regexp.Regexp
}{
{"pipe-to-shell", regexp.MustCompile(`(?i)\b(curl|wget)\b[^|\n]*\|\s*(sudo\s+)?(ba|z)?sh\b`)},
{"base64-to-shell", regexp.MustCompile(`(?i)\bbase64\b\s+(-d|-D|--decode)\b[^\n]*\|\s*(ba|z)?sh\b`)},
{"prompt-injection", regexp.MustCompile(`(?i)\b(ignore|disregard)\s+(all\s+)?(previous|prior|above)\s+(instructions|context|rules)\b`)},
{"hidden-from-user", regexp.MustCompile(`(?i)\bdo not (tell|inform|mention|reveal)( this)?( to)? the user\b`)},
{"credential-paths", regexp.MustCompile(`(\$HOME|~)/\.(ssh|aws|gnupg|netrc)\b`)},
}

var skillAuditExtensions = map[string]bool{
".md": true, ".sh": true, ".bash": true, ".zsh": true,
".py": true, ".js": true, ".mjs": true, ".ts": true,
}

const skillAuditMaxFileSize = 1 << 20 // 1 MiB

// auditSkillTree scans one skill directory and returns findings as
// "relpath: pattern" strings.
func auditSkillTree(root string) ([]string, error) {
var findings []string
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if strings.HasPrefix(d.Name(), ".") && path != root {
return filepath.SkipDir
}
return nil
}
if !skillAuditExtensions[strings.ToLower(filepath.Ext(d.Name()))] {
return nil
}
info, err := d.Info()
if err != nil || info.Size() > skillAuditMaxFileSize {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
rel, relErr := filepath.Rel(root, path)
if relErr != nil {
rel = path
}
for _, pattern := range skillAuditPatterns {
if pattern.re.Match(data) {
findings = append(findings, fmt.Sprintf("%s: %s", rel, pattern.name))
}
}
return nil
})
return findings, err
}

func checkExternalSkillAudit(cfg config, home string) checkResult {
const checkName = "external skill audit"
if len(cfg.ExternalSkills) == 0 {
return checkResult{checkName, checkStatusPass, "none configured"}
}
skills, err := discoverExternalSkills(cfg.ExternalSkills, home)
if err != nil {
return checkResult{checkName, checkStatusWarn, fmt.Sprintf("cannot discover external skills: %v", err)}
}
var findings []string
names := make([]string, 0, len(skills))
for name := range skills {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
hits, err := auditSkillTree(skills[name])
if err != nil {
return checkResult{checkName, checkStatusWarn, fmt.Sprintf("scan %s: %v", name, err)}
}
for _, hit := range hits {
findings = append(findings, fmt.Sprintf("%s/%s", name, hit))
}
}
if len(findings) > 0 {
return checkResult{checkName, checkStatusWarn, "review: " + strings.Join(findings, "; ")}
}
return checkResult{checkName, checkStatusPass, fmt.Sprintf("%d skills scanned, no risky patterns", len(skills))}
}

func checkExternalSkillLock(repoRoot string, cfg config, home string) checkResult {
const checkName = "external skill lock"
if len(cfg.ExternalSkills) == 0 {
return checkResult{checkName, checkStatusPass, "none configured"}
}
lock, err := readLockFile(repoRoot)
if err != nil {
return checkResult{checkName, checkStatusWarn, err.Error()}
}
cacheRoot := externalCacheDir(home)
var issues []string
pinned := 0
for _, src := range cfg.ExternalSkills {
name := repoName(src.URL)
pin := lockEntryFor(lock, src)
if pin == nil {
issues = append(issues, fmt.Sprintf("%s unpinned (run dotagents sync)", name))
continue
}
head := externalSkillCommitFull(filepath.Join(cacheRoot, name))
if head == "" {
issues = append(issues, fmt.Sprintf("%s not cloned", name))
continue
}
if head != pin.Commit {
issues = append(issues, fmt.Sprintf("%s cache at %s but lock pins %s", name, shortCommit(head), shortCommit(pin.Commit)))
continue
}
pinned++
}
if len(issues) > 0 {
return checkResult{checkName, checkStatusWarn, strings.Join(issues, "; ")}
}
return checkResult{checkName, checkStatusPass, fmt.Sprintf("%d sources pinned and in sync", pinned)}
}
Loading
Loading