Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Default owners for everything in the repo.
# These owners will be requested for review on all pull requests.
* @buckle-dev/maintainers
* @marceloceccon

# Core team members can be added here as the project grows.
# Example:
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ A clear description of what you expected to happen.
**Environment**
- OS: [e.g. macOS 15, Ubuntu 24.04, Windows 11]
- Node version: [e.g. 22.3.0]
- buckle version: [e.g. 0.1.0]
- buckle version: [e.g. 0.4.0]
- Docker / Podman version:

**Additional context**
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ jobs:
- name: Typecheck
run: npm run typecheck

- name: Test
run: npm test
- name: Test with coverage
run: npm run test:coverage

- name: Upload coverage
if: matrix.node-version == '22'
Expand Down
67 changes: 54 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,48 @@ All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] — 2026-05-28
## [Unreleased]

## [0.4.0] — 2026-08-31

### Breaking

- **Per-workspace isolation is now the default.** `~/.claude` and `~/.grok` are
no longer bind-mounted from the host when rendering. Each workspace gets its
own skills/config under `$XDG_DATA_HOME/buckle/workspaces/<id>/`.
Pass `--share-home` (or `--no-isolate`, or `isolate: false` in
`~/.config/buckle/config.yaml`) to restore the previous host-home mounts.
Existing `.devcontainer/` files are unchanged until you re-render
(`buckle <template> --yes` or status-panel `c`).
- Non-interactive trust failures tell you to pass `--trust` after `buckle view`.
Interactive TTYs now actually prompt `[y/N]` and show the executable surface.
- `buckle install` refuses plaintext `http://` origins.
- MCP convenience features no longer swallow `npm install` failures (`|| true` removed).

### Added

- Trusted Publishing support via GitHub Actions OIDC (no more long-lived NPM_TOKEN required).
- Proper release workflow (`.github/workflows/release.yml`) triggered on version tags.
- Code scanning fixes for path traversal vulnerabilities (in template installation and `buckle new`).
- Improved pre-commit lint guard using Husky + lint-staged.
- Better Dependabot configuration to reduce noise on risky major updates.
- `--share-home` / `--no-isolate` to opt out of isolation.
- `~/.config/buckle/config.yaml` is loaded (`editor`, `defaultTemplate`, `isolate`).
- Status panel `c` reconfigure flow (pick a template, rewrite `.devcontainer/`).
- Dual-agent callouts for `ai-native` and `claude-corp` in `buckle list`,
`buckle doctor`, and the wizard.
- Empty-directory autodetect suggests `ai-native` first.
- Interactive lifecycle trust prompt for user/installed templates.
- `docs/DESIGN-NOTES.md`, `MAINTAINERS.md`.

### Changed
### Fixed

- Package is now published as `buckle-cli` on npm (binary remains `buckle`).
- Repository references updated for current fork during initial development.
- Various CI and developer experience improvements (lint config, version validation on release).
- `--version` reports the `package.json` version instead of a stale `0.1.0`.
- Trust-surface hashes use a key-sorted serializer (no false “changed” from key order).
- Unknown convenience features raise `BuckleError` (`E_TEMPLATE_INVALID`).
- Generated file banners and docs point at `entropyvortex/buckle`.
- CI runs coverage thresholds. Code scanning uses GitHub's default CodeQL setup.

## [Unreleased]
### Security

- Path-stable origin parsing and ReDoS hardening remain as of 0.3.0.
- Hook-surface hashing is now order-independent.
- Install origins cannot be plaintext HTTP.

## [0.3.0] — 2026-05-28

Expand All @@ -42,6 +67,20 @@ These changes address GitHub CodeQL alerts ("Uncontrolled data used in path expr

## [0.2.0] — 2026-05-28

### Added

- Trusted Publishing support via GitHub Actions OIDC (no more long-lived NPM_TOKEN required).
- Proper release workflow (`.github/workflows/release.yml`) triggered on version tags.
- Code scanning fixes for path traversal vulnerabilities (in template installation and `buckle new`).
- Improved pre-commit lint guard using Husky + lint-staged.
- Better Dependabot configuration to reduce noise on risky major updates.

### Changed

- Package is now published as `buckle-cli` on npm (binary remains `buckle`).
- Repository references updated for current fork during initial development.
- Various CI and developer experience improvements (lint config, version validation on release).

## [0.1.0] — 2026-05-08

### Added
Expand All @@ -64,6 +103,8 @@ These changes address GitHub CodeQL alerts ("Uncontrolled data used in path expr
- `buckle install` from `gh:user/repo`, `gl:user/repo`, https git URL, or `file://`.
- `--preview` / `--dry-run` to emit a diff without writing.
- Stable JSON error codes.
- 179 unit + integration tests; ≥ 90% coverage.

[0.1.0]: https://github.com/buckle-dev/buckle/releases/tag/v0.1.0
[0.4.0]: https://github.com/entropyvortex/buckle/releases/tag/v0.4.0
[0.3.0]: https://github.com/entropyvortex/buckle/releases/tag/v0.3.0
[0.2.0]: https://github.com/entropyvortex/buckle/releases/tag/v0.2.0
[0.1.0]: https://github.com/entropyvortex/buckle/releases/tag/v0.1.0
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Thank you for your interest in contributing to buckle!
## Development Setup

```bash
git clone https://github.com/buckle-dev/buckle.git
git clone https://github.com/entropyvortex/buckle.git
cd buckle
npm install
npm run build
Expand Down
11 changes: 11 additions & 0 deletions MAINTAINERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Maintainers

Security and release decisions for this repository.

| Name | Email | GitHub |
| --- | --- | --- |
| Marcelo Ceccon | marcelo@ceccon.org | [@marceloceccon](https://github.com/marceloceccon) |

GPG keys for encrypted security reports: request via the email above.

Releases are cut from `main` by a maintainer (see CONTRIBUTING.md).
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ $ buckle up claude-corp # Node + Claude Code + GitHub CLI + selected MCPs
| `--detach` | Don't attach a shell after `up`/`rebuild`. |
| `--git-init` | Initialize a git repo in the workspace if there is none. |
| `--preview`, `--dry-run` | Show what *would* be written without touching disk. |
| `--isolate` | Default. Per-workspace `~/.claude` / `~/.grok` under `$XDG_DATA_HOME/buckle/workspaces/`. |
| `--share-home`, `--no-isolate` | Bind-mount host `~/.claude`, `~/.grok`, and `~/.gitconfig` instead. |

---

Expand Down Expand Up @@ -474,7 +476,7 @@ status: running
image: mcr.microsoft.com/devcontainers/javascript-node:1-20-bullseye
ports: 3000 → 3000/tcp

r rebuild · u up · s/d down · b bash · q quit
r rebuild · u up · s/d down · b bash · c reconfigure · q quit
```

Refresh interval defaults to 5 s; override with `BUCKLE_STATUS_REFRESH=2000`.
Expand Down Expand Up @@ -519,17 +521,47 @@ file system. `buckle` therefore prompts you on first use of any template whose
is unseen. The trust store lives at `~/.config/buckle/trust.json` and maps the merged-template
SHA-256 to the date you trusted it. If the surface changes, you're prompted again.

On a TTY, buckle prints that surface and asks `[y/N]`. In CI, `--json`, or any non-TTY
context the prompt is a hard error (`E_HASH_MISMATCH`); pass `--trust` after reviewing
with `buckle view`.

You can:

- pass `--trust` to skip the prompt for that one run
- inspect a template before trusting with `buckle edit <name>`
- inspect a template before trusting with `buckle view <name>` / `buckle edit <name>`
- review what would be written without committing with `--preview` / `--dry-run`

Built-in templates are pre-trusted. User and installed templates are not.

`buckle` does no network access during template resolution. `buckle install` does network
clones via `git`. `buckle doctor` is the only command that probes outside the workspace.
clones via `git` (HTTPS only; plaintext `http://` is rejected). `buckle doctor` is the only
command that probes outside the workspace.

See [SECURITY.md](SECURITY.md) for the full threat model.

## Isolation (default)

Agent skills, versions, and config are **per workspace**, not shared from the host:

| Path in container | Isolated source (default) | `--share-home` |
| --- | --- | --- |
| `/home/vscode/.claude` | `$XDG_DATA_HOME/buckle/workspaces/<slug>-<hash>/claude` | `${localEnv:HOME}/.claude` |
| `/home/vscode/.grok` | `$XDG_DATA_HOME/buckle/workspaces/<slug>-<hash>/grok` | `${localEnv:HOME}/.grok` |
| `/home/vscode/.gitconfig` | host `~/.gitconfig` (identity, always) | host `~/.gitconfig` |

Re-render an existing project to pick up isolation:

```bash
buckle ai-native --yes # isolated (default)
buckle ai-native --share-home --yes
```

Override for every command via `~/.config/buckle/config.yaml`:

```yaml
isolate: false # equivalent to always passing --share-home
```

---

## Compatibility
Expand Down Expand Up @@ -561,6 +593,7 @@ Per-user file at `~/.config/buckle/config.yaml` (optional):
```yaml
editor: code # falls back to $VISUAL → $EDITOR → vi
defaultTemplate: node # used by the wizard if autodetect can't decide
isolate: true # default; set false to share host ~/.claude and ~/.grok
```

Environment variables:
Expand All @@ -572,12 +605,13 @@ Environment variables:
| `BUCKLE_BUILTIN_DIR` | Override built-in template directory (testing). |
| `BUCKLE_NO_COLOR` | Strip ANSI from output (also honors `NO_COLOR`). |
| `BUCKLE_DEBUG` | Print full stack traces on uncaught errors. |
| `BUCKLE_STATE_DIR` | Override the per-workspace AI state root (default `$XDG_DATA_HOME/buckle/workspaces`). |

---

## Testing

Run the full suite (179 tests at the time of writing):
Run the full suite:

```bash
npm test
Expand Down Expand Up @@ -646,7 +680,7 @@ change the spec, please reference it.
To work on buckle locally:

```bash
git clone https://github.com/buckle-dev/buckle.git
git clone https://github.com/entropyvortex/buckle.git
cd buckle
npm install
npm run build
Expand Down
15 changes: 9 additions & 6 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ the latest minor of the current major and the previous major receive fixes for 6
## Reporting a vulnerability

**Please do not open a public issue for security reports.** Instead, email
**security@buckle.dev** with:
**marcelo@ceccon.org** (or **security@buckle.dev** if that mailbox is reachable) with:

- a clear description of the issue,
- a minimal reproducer,
Expand Down Expand Up @@ -56,8 +56,12 @@ that has been granted host access (e.g., via `dod` or bind-mounts).
- `buckle edit <name>` opens the template for review before you trust it.
- `buckle <name> --preview` (or `--dry-run`) emits the full diff of generated files without
touching disk.
- The trust prompt is **non-interactive-aware**: in CI / non-TTY contexts, the prompt becomes a
hard error (`E_HASH_MISMATCH`); you must opt in explicitly with `--trust`.
- On a TTY, buckle prints the executable surface and prompts `[y/N]`.
- In CI / `--json` / non-TTY contexts the prompt becomes a hard error (`E_HASH_MISMATCH`);
you must opt in explicitly with `--trust`.
- Built-in templates are pre-trusted. User and installed templates are not.
- Agent config dirs (`~/.claude`, `~/.grok`) are isolated per workspace by default so
skills and credentials do not leak across containers. `--share-home` restores host binds.

**Out of scope**: We do *not* statically analyze the body of `postCreate` commands. The trust
boundary is your eyes — the same as `make` or `npm install`.
Expand Down Expand Up @@ -122,6 +126,5 @@ resolution.

## Contact

- General security issues: **security@buckle.dev**
- Public discussion: `#security` in the project chat
- Maintainer GPG keys: Available upon request via the security email above.
- General security issues: **marcelo@ceccon.org**
- Maintainer GPG keys: listed in [MAINTAINERS.md](MAINTAINERS.md) (available on request)
62 changes: 62 additions & 0 deletions docs/DESIGN-NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Design notes

This is the architecture consensus for buckle: a single TypeScript CLI that
collapses generate → build → up → bash into one verb, with **user-wide**
templates rather than per-repo copy-paste.

## Core bets

1. **Templates travel with the developer.** `~/.config/buckle/templates/` is the
source of truth. Repos receive generated `.devcontainer/` artifacts, not the
template itself.
2. **One schema, three artifacts.** A YAML template compiles deterministically
to `devcontainer.json` plus optional `Dockerfile` / `docker-compose.yml`.
3. **Inheritance is data, not a preprocessor.** `extends` is a string or ordered
list; objects deep-merge; arrays append unless the child starts with
`!replace`. Cycles are errors. Depth is capped at 8.
4. **Convenience features are sugar.** `dod`, `claude-code`, `grok`, `mcp:*`
compile to native devcontainer features + hooks + mounts. Unknown names are
`E_TEMPLATE_INVALID`.
5. **The executable surface is a trust boundary.** Lifecycle, mounts, runArgs,
and features are hashed with a stable serializer. Built-ins are pre-trusted.
User/installed templates prompt interactively (or require `--trust` when
non-TTY).
6. **AI agent state is per-workspace by default.** Host `~/.claude` and `~/.grok`
are not bind-mounted unless `--share-home`. Isolated state lives under
`$XDG_DATA_HOME/buckle/workspaces/<slug>-<hash>/{claude,grok}` so skills,
versions, and configs do not leak across containers. Host `~/.gitconfig`
remains shared (identity, not agent state).

## Layers

```
cli/ parse, JSON envelope, command shells, render pipeline
templates/ zod schema, loader, resolver, autodetect, trust, isolate
features/ convenience catalog → native features + hooks
generators/ devcontainer / Dockerfile / compose + plan/apply
docker/ naming, inspect, @devcontainers/cli wrapper, Driver
tui/ wizard (no .devcontainer) / status panel (has one)
util/ errors, paths (XDG), log, fs, slug, config, version
```

## Non-goals (0.x)

- Signature verification of installed templates (`metadata.signature` is reserved).
- Static analysis of hook command bodies.
- First-class Podman (Docker-API compat only; `doctor` warns).
- Per-step `user:` on lifecycle hooks (`@devcontainers/cli` rejects the shape).

## CLI contract

- Every command accepts `--json` with a stable envelope (`ok`, `timestamp`,
`workspace`, `data` | `error.code`).
- Error codes in `src/util/errors.ts` are never repurposed.
- `--version` is read from `package.json` (`buckle-cli`), not a string literal.
- `--preview` / `--dry-run` never write.

## Invariants

- At most one of `image` / `build` / `compose` after merge.
- Generated JSON is key-sorted for stable diffs.
- Writes go through `writeTextAtomic`.
- Isolation dirs are created at plan time so `devcontainer up` can bind them.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "buckle-cli",
"version": "0.3.0",
"version": "0.4.0",
"description": "One verb for devcontainers — generate, build, up, and bash with user-wide templates.",
"license": "MIT",
"author": "Buckle Contributors",
Expand Down
17 changes: 17 additions & 0 deletions src/cli/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { execa } from 'execa';

import { DockerCli } from '../../docker/inspect.js';
import { hasDevcontainerCli } from '../../docker/devcontainer-cli.js';
import { DUAL_AGENT_TEMPLATES } from '../../templates/ai.js';
import { workspaceStateDir } from '../../templates/isolate.js';
import { exists } from '../../util/fs.js';
import { bucklePaths } from '../../util/paths.js';
import { styles } from '../../util/log.js';
Expand Down Expand Up @@ -93,6 +95,21 @@ export async function runDoctor(ctx: CliContext): Promise<number> {
checks.push({ name: 'docker.buildx', status: 'warn', message: 'docker buildx not available' });
}

const isolateOn = ctx.flags.isolate !== false;
checks.push({
name: 'ai.isolate',
status: 'pass',
message: isolateOn
? `per-workspace agent state (${workspaceStateDir(ctx.cwd)}; --share-home to bind host ~/.claude and ~/.grok)`
: 'sharing host ~/.claude and ~/.grok (--share-home)',
});

checks.push({
name: 'ai.templates',
status: 'pass',
message: `dual-agent ready: ${DUAL_AGENT_TEMPLATES.join(', ')} (Claude Code + Grok Build)`,
});

const overall = checks.some((c) => c.status === 'fail')
? 'broken'
: checks.some((c) => c.status === 'warn')
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export async function runEdit(ctx: CliContext, args: { template: string }): Prom
`run "buckle view ${args.template}" to inspect, or "buckle new ${args.template} --extend ${args.template}" to create an editable copy`,
);
}
const editor = process.env['VISUAL'] ?? process.env['EDITOR'] ?? 'vi';
const editor = ctx.config.editor ?? process.env['VISUAL'] ?? process.env['EDITOR'] ?? 'vi';
return new Promise((resolveP) => {
const proc = spawn(editor, [rec.path], { stdio: 'inherit' });
proc.on('exit', (code) => resolveP(code ?? 0));
Expand Down
Loading
Loading