🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

codojo

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

codojo - npm Package Compare versions

Comparing version
0.1.0
to
0.2.0
+9
dist/exitCode.d.ts
/**
* 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
{
"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": [

@@ -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