| /** | ||
| * Map a thrown error to the process exit code codojo should terminate with. | ||
| * | ||
| * `@inquirer/prompts` throws an `ExitPromptError` when the user cancels a prompt | ||
| * with Ctrl-C; that is a clean cancellation, which by convention exits `130`. | ||
| * Every other error is a genuine failure and exits `1`. | ||
| */ | ||
| export declare function exitCodeForError(err: unknown): number; | ||
| //# sourceMappingURL=exitCode.d.ts.map |
| /** | ||
| * Map a thrown error to the process exit code codojo should terminate with. | ||
| * | ||
| * `@inquirer/prompts` throws an `ExitPromptError` when the user cancels a prompt | ||
| * with Ctrl-C; that is a clean cancellation, which by convention exits `130`. | ||
| * Every other error is a genuine failure and exits `1`. | ||
| */ | ||
| export function exitCodeForError(err) { | ||
| if (err instanceof Error && err.name === 'ExitPromptError') { | ||
| return 130; | ||
| } | ||
| return 1; | ||
| } | ||
| //# sourceMappingURL=exitCode.js.map |
+11
-6
| #!/usr/bin/env node | ||
| import chalk from 'chalk'; | ||
| import { runInit } from './commands/init.js'; | ||
| import { exitCodeForError } from './exitCode.js'; | ||
| const USAGE = `${chalk.bold('codojo')} — an AI-powered coding dojo for learning new languages | ||
@@ -10,3 +11,5 @@ | ||
| ${chalk.bold('Commands')} | ||
| init [dir] Scaffold a new learning workspace (default: ~/workspace/codojo) | ||
| init [dir] [--allow-gh-cli] | ||
| Scaffold a new learning workspace (default: ~/workspace/codojo). | ||
| --allow-gh-cli enables read-only GitHub CLI access for the mentor. | ||
| help Show this help | ||
@@ -34,10 +37,12 @@ | ||
| main().catch((err) => { | ||
| // @inquirer throws ExitPromptError when the user hits Ctrl-C at a prompt. | ||
| if (err instanceof Error && err.name === 'ExitPromptError') { | ||
| const code = exitCodeForError(err); | ||
| if (code === 130) { | ||
| // @inquirer throws ExitPromptError when the user hits Ctrl-C at a prompt. | ||
| console.log(chalk.dim('\nCancelled.')); | ||
| process.exit(130); | ||
| } | ||
| console.error(chalk.red(err instanceof Error ? err.message : String(err))); | ||
| process.exit(1); | ||
| else { | ||
| console.error(chalk.red(err instanceof Error ? err.message : String(err))); | ||
| } | ||
| process.exit(code); | ||
| }); | ||
| //# sourceMappingURL=cli.js.map |
@@ -8,6 +8,7 @@ /** | ||
| * | ||
| * @param argv extra CLI args after the `init` subcommand; the first, if given, | ||
| * is the workspace directory. | ||
| * @param argv extra CLI args after the `init` subcommand: the first non-flag arg | ||
| * is the workspace directory; `--allow-gh-cli` opts into read-only | ||
| * GitHub CLI access (recognized in any position). | ||
| */ | ||
| export declare function runInit(argv?: string[]): Promise<void>; | ||
| //# sourceMappingURL=init.d.ts.map |
@@ -22,7 +22,9 @@ import path from 'node:path'; | ||
| * | ||
| * @param argv extra CLI args after the `init` subcommand; the first, if given, | ||
| * is the workspace directory. | ||
| * @param argv extra CLI args after the `init` subcommand: the first non-flag arg | ||
| * is the workspace directory; `--allow-gh-cli` opts into read-only | ||
| * GitHub CLI access (recognized in any position). | ||
| */ | ||
| export async function runInit(argv = []) { | ||
| const provided = argv[0]; | ||
| const allowGhCli = argv.includes('--allow-gh-cli'); | ||
| const provided = argv.find((arg) => !arg.startsWith('--')); | ||
| const raw = provided ?? | ||
@@ -34,2 +36,11 @@ (await input({ | ||
| const workspace = expandHome(raw); | ||
| if (await fs.pathExists(workspace)) { | ||
| const stats = await fs.stat(workspace); | ||
| if (!stats.isDirectory()) { | ||
| console.error(chalk.red(`\n✗ ${workspace} already exists and is not a directory.`)); | ||
| console.error(chalk.dim(' Pick a different path for your codojo workspace.')); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
| } | ||
| if (await isNonEmptyDir(workspace)) { | ||
@@ -43,3 +54,3 @@ console.error(chalk.red(`\n✗ ${workspace} already exists and is not empty.`)); | ||
| console.log(chalk.dim(`\nScaffolding workspace at ${workspace} …`)); | ||
| for (const file of workspaceFiles()) { | ||
| for (const file of workspaceFiles({ allowGhCli })) { | ||
| const dest = path.join(workspace, file.path); | ||
@@ -46,0 +57,0 @@ await fs.ensureDir(path.dirname(dest)); |
@@ -101,2 +101,10 @@ /** | ||
| boundaries, but you should honor them in spirit even where enforcement is soft. | ||
| **Writing files:** the sandbox denies *all* shell writes in this workspace, so | ||
| never use shell commands like \`touch\`, \`>\`, \`mv\`, or \`rm\` to create, change, or | ||
| delete files. Use your Edit/Write tools instead — they are permitted for | ||
| \`mentor_notes/\`, \`profile.md\`, and \`goals.md\`. If a task genuinely needs a | ||
| write-performing shell command (including deleting a file), don't attempt it | ||
| yourself — give the learner the exact command and let them run it (with the \`!\` | ||
| prefix or in their own terminal). | ||
| `; | ||
@@ -103,0 +111,0 @@ } |
@@ -0,1 +1,2 @@ | ||
| import type { WorkspaceOptions } from '../types/index.js'; | ||
| /** A single file to write into the workspace, relative to its root. */ | ||
@@ -9,3 +10,3 @@ export interface WorkspaceFile { | ||
| /** Every file `init` writes into a fresh workspace. */ | ||
| export declare function workspaceFiles(): WorkspaceFile[]; | ||
| export declare function workspaceFiles(options?: WorkspaceOptions): WorkspaceFile[]; | ||
| //# sourceMappingURL=index.d.ts.map |
@@ -11,6 +11,6 @@ /** | ||
| /** Every file `init` writes into a fresh workspace. */ | ||
| export function workspaceFiles() { | ||
| export function workspaceFiles(options = {}) { | ||
| return [ | ||
| { path: 'CLAUDE.md', content: rootClaudeMd() }, | ||
| { path: '.claude/settings.json', content: settingsJson() }, | ||
| { path: '.claude/settings.json', content: settingsJson(options) }, | ||
| { path: 'profile.md', content: profileMd() }, | ||
@@ -17,0 +17,0 @@ { path: 'goals.md', content: goalsMd() }, |
| /** | ||
| * Contents of `<workspace>/.claude/settings.json`. | ||
| * | ||
| * Enforces the codojo permission boundaries: | ||
| * - `notes/` and `projects/` are READ-ONLY for the mentor (deny edits). | ||
| * - `mentor_notes/`, `profile.md`, `goals.md` are READ/WRITE. | ||
| * - Common secret locations and parent-directory traversal are denied. | ||
| * Two layers of confinement: | ||
| * - Permission rules govern Claude's own file tools: `notes/` and `projects/` | ||
| * are read-only; `mentor_notes/`, `profile.md`, `goals.md` are writable; | ||
| * secret locations are denied. | ||
| * - An OS-level `sandbox` block confines Bash subprocesses to the workspace — | ||
| * reads denied outside it (`denyRead: ["/"]` + `allowRead: ["."]`) and ALL | ||
| * shell writes denied (`denyWrite: ["."]`; `allowWrite` cannot re-permit | ||
| * within it). Claude Code maps this single declarative block to Seatbelt | ||
| * (macOS) / bubblewrap (Linux/WSL2). | ||
| * | ||
| * Caveat: the workspace usually lives inside the user's home directory, so we | ||
| * cannot blanket-deny `~/**` without locking the workspace out of itself. These | ||
| * rules constrain Claude's own file tools; for OS-level isolation of arbitrary | ||
| * Bash subprocesses, enable `sandbox.filesystem`. See README "Permission model". | ||
| * With `allowGhCli`, the GitHub CLI is allowed to run OUTSIDE the sandbox | ||
| * (`excludedCommands`, because `gh` fails TLS under Seatbelt) and a closed set of | ||
| * read-only `gh` subcommands is auto-approved. `gh auth` is deliberately excluded | ||
| * to avoid `--show-token` leaking the token. By default `gh` is blocked entirely. | ||
| * | ||
| * The sandbox is the real boundary; permission rules only bind the agent's own | ||
| * tools, not the shell subprocesses it spawns. See README "Permission model". | ||
| */ | ||
| export declare function settingsJson(): string; | ||
| import type { WorkspaceOptions } from '../types/index.js'; | ||
| export declare function settingsJson({ allowGhCli, }?: WorkspaceOptions): string; | ||
| //# sourceMappingURL=settings.d.ts.map |
@@ -1,15 +0,16 @@ | ||
| /** | ||
| * Contents of `<workspace>/.claude/settings.json`. | ||
| * | ||
| * Enforces the codojo permission boundaries: | ||
| * - `notes/` and `projects/` are READ-ONLY for the mentor (deny edits). | ||
| * - `mentor_notes/`, `profile.md`, `goals.md` are READ/WRITE. | ||
| * - Common secret locations and parent-directory traversal are denied. | ||
| * | ||
| * Caveat: the workspace usually lives inside the user's home directory, so we | ||
| * cannot blanket-deny `~/**` without locking the workspace out of itself. These | ||
| * rules constrain Claude's own file tools; for OS-level isolation of arbitrary | ||
| * Bash subprocesses, enable `sandbox.filesystem`. See README "Permission model". | ||
| */ | ||
| export function settingsJson() { | ||
| /** Read-only `gh` subcommands auto-approved when `--allow-gh-cli` is set (FR-009). */ | ||
| const GH_READONLY_RULES = [ | ||
| 'Bash(gh pr view:*)', | ||
| 'Bash(gh pr list:*)', | ||
| 'Bash(gh pr diff:*)', | ||
| 'Bash(gh pr checks:*)', | ||
| 'Bash(gh issue view:*)', | ||
| 'Bash(gh issue list:*)', | ||
| 'Bash(gh repo view:*)', | ||
| 'Bash(gh run view:*)', | ||
| 'Bash(gh run list:*)', | ||
| 'Bash(gh search:*)', | ||
| 'Bash(gh status:*)', | ||
| ]; | ||
| export function settingsJson({ allowGhCli = false, } = {}) { | ||
| const settings = { | ||
@@ -26,2 +27,3 @@ permissions: { | ||
| 'Write(goals.md)', | ||
| ...(allowGhCli ? GH_READONLY_RULES : []), | ||
| ], | ||
@@ -35,3 +37,2 @@ deny: [ | ||
| 'MultiEdit(projects/**)', | ||
| 'Read(../**)', | ||
| 'Read(~/.ssh/**)', | ||
@@ -43,2 +44,14 @@ 'Read(~/.aws/**)', | ||
| }, | ||
| sandbox: { | ||
| enabled: true, | ||
| allowUnsandboxedCommands: false, | ||
| // Only present when gh is opted in; keeps the key after allowUnsandboxedCommands. | ||
| ...(allowGhCli ? { excludedCommands: ['gh *'] } : {}), | ||
| filesystem: { | ||
| allowRead: ['.'], | ||
| denyRead: ['/'], | ||
| denyWrite: ['.'], | ||
| allowWrite: ['./mentor_notes', './profile.md', './goals.md', '/tmp'], | ||
| }, | ||
| }, | ||
| }; | ||
@@ -45,0 +58,0 @@ return JSON.stringify(settings, null, 2) + '\n'; |
@@ -42,2 +42,9 @@ /** | ||
| } | ||
| /** Options chosen at `init` time that influence the generated workspace. */ | ||
| export interface WorkspaceOptions { | ||
| /** Opt in to read-only GitHub CLI (`gh`) access for the mentor. Default false. */ | ||
| allowGhCli?: boolean; | ||
| } | ||
| /** Default {@link WorkspaceOptions}: every opt-in capability off. */ | ||
| export declare const DEFAULT_WORKSPACE_OPTIONS: Required<WorkspaceOptions>; | ||
| //# sourceMappingURL=index.d.ts.map |
@@ -9,3 +9,6 @@ /** | ||
| */ | ||
| export {}; | ||
| /** Default {@link WorkspaceOptions}: every opt-in capability off. */ | ||
| export const DEFAULT_WORKSPACE_OPTIONS = { | ||
| allowGhCli: false, | ||
| }; | ||
| //# sourceMappingURL=index.js.map |
+1
-1
| { | ||
| "name": "codojo", | ||
| "version": "0.1.0", | ||
| "version": "0.2.0", | ||
| "description": "AI-powered coding dojo for developers learning new languages — concept mapping, guided practice, and quizzing", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
+35
-6
@@ -1,2 +0,4 @@ | ||
| # codojo | ||
| <p align="center"> | ||
| <img src=".github/assets/codojo_logo.png" alt="codojo" width="440"> | ||
| </p> | ||
@@ -57,8 +59,26 @@ **codojo** is an AI-powered coding dojo — a mentor for developers learning a new | ||
| `.claude/settings.json` makes `notes/` and `projects/` read-only to the mentor | ||
| and `mentor_notes/` (plus `profile.md`/`goals.md`) read/write, and denies a few | ||
| sensitive paths. Because a workspace typically lives inside your home directory, | ||
| these rules constrain Claude's own file tools but are not OS-level isolation — | ||
| for that, enable Claude Code's `sandbox.filesystem`. | ||
| A generated workspace is confined two ways: | ||
| - **OS-level sandbox (the real boundary).** `.claude/settings.json` declares a | ||
| `sandbox` block that Claude Code enforces at the OS level (Seatbelt on macOS, | ||
| bubblewrap on Linux/WSL2): the mentor's shell commands can only **read** inside | ||
| the workspace, and **all shell writes are denied**. This holds regardless of | ||
| what the mentor runs — it isn't just a guardrail on Claude's own tools. | ||
| - **Permission rules (tool-level).** The same file keeps `notes/` and `projects/` | ||
| read-only to the mentor and lets it write `mentor_notes/`, `profile.md`, and | ||
| `goals.md` — but these rules bind only Claude's own file tools, not the shell | ||
| subprocesses it spawns, so they are backed by the sandbox rather than relied on | ||
| alone. | ||
| Because shell writes are denied workspace-wide, the mentor changes its own files | ||
| through its Edit/Write tools and hands any write-needing shell command to you to | ||
| run. Network tools are blocked too, with one opt-in exception: | ||
| - **`codojo init --allow-gh-cli`** enables a closed, **read-only** set of GitHub | ||
| CLI lookups (viewing/listing PRs, issues, and runs; searching; repo and status | ||
| views). `gh` runs outside the sandbox (it can't complete TLS inside it), so the | ||
| flag is a deliberate, opt-in widening of the boundary; mutating and `gh auth` | ||
| commands still require your approval. Without the flag, `gh` is blocked | ||
| entirely. | ||
| ## Requirements | ||
@@ -69,4 +89,13 @@ | ||
| ## Contributing | ||
| codojo is built with [Spec-Driven Development](https://github.com/github/spec-kit). | ||
| Note that `.claude/` (the spec-kit slash-command skills) is git-ignored, so after | ||
| cloning you'll need to regenerate it locally with `specify init . --integration | ||
| claude --force`. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full setup, | ||
| including an important caveat about not overwriting the committed `.specify/` | ||
| config. | ||
| ## License | ||
| MIT © 2026 Jason Noble |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
34860
18.03%25
8.7%607
14.74%100
40.85%