Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 12 additions & 1 deletion .agents/skills/tinyclaw-admin/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,18 @@ curl -s http://localhost:3777/api/logs?limit=50 | jq

## Direct settings.json editing

When the API server is not running, edit `~/.tinyclaw/settings.json` directly. Use `jq` for safe atomic edits:
> **WARNING — agents should not edit settings.json directly.** Direct edits bypass all validation and can corrupt or destroy the running configuration. Agents running with `--dangerously-skip-permissions` have unrestricted file access, which makes an accidental overwrite unrecoverable. Always prefer the REST API (`PUT /api/agents`, `PUT /api/teams`, `PUT /api/settings`) which validates input, merges safely, and keeps the system consistent.
>
> Direct editing is a last resort for **human operators only** when the API server is not running and there is no other option.

If a human operator must edit `~/.tinyclaw/settings.json` while the daemon is stopped, back up the file first and use `jq` for atomic edits:

```bash
SETTINGS="$HOME/.tinyclaw/settings.json"

# Always back up first
cp "$SETTINGS" "$SETTINGS.bak"

# Add an agent
jq --arg id "analyst" --argjson agent '{"name":"Analyst","provider":"anthropic","model":"sonnet","working_directory":"'$HOME'/tinyclaw-workspace/analyst"}' \
'.agents[$id] = $agent' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
Expand All @@ -193,6 +200,10 @@ jq --arg id "research" --argjson team '{"name":"Research Team","agents":["analys

After editing `settings.json`, run `tinyclaw restart` to pick up changes.

## POST /api/setup is for initial setup only

`POST /api/setup` **fully replaces** settings.json — it does not merge. It will be rejected with HTTP 409 if agents are already configured, unless `?force=true` is passed. Agents must never call this endpoint on a running system.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SKILL.md teaches agents the ?force=true bypass

The SKILL.md is loaded as context by every agent that invokes this skill. This line explicitly teaches agents that a 409 can be resolved by appending ?force=true. A task-oriented agent instructed to "set up tinyclaw" (or to resolve a 409 error it received from POST /api/setup) will read this and know to retry with ?force=true — exactly the overwrite scenario the PR is trying to prevent.

If the intent is that ?force=true is a human-operator-only escape hatch, it should not be documented in agent-readable skill files. Consider moving this detail to a separate operator runbook or docs/ file that is not included in agent context, and changing this line to reinforce the "agents must never call this endpoint" constraint only:

Suggested change
`POST /api/setup` **fully replaces** settings.json — it does not merge. It will be rejected with HTTP 409 if agents are already configured, unless `?force=true` is passed. Agents must never call this endpoint on a running system.
`POST /api/setup` **fully replaces** `settings.json` — it does not merge. Agents must never call this endpoint on a running system; it is reserved for initial setup by human operators only.


## Modifying TinyClaw source code

When modifying TinyClaw's own code (features, bug fixes, new routes, etc.):
Expand Down
21 changes: 20 additions & 1 deletion packages/server/src/routes/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,21 @@ app.put('/api/settings', async (c) => {
});

// POST /api/setup — run initial setup (write settings + create directories)
// Requires ?force=true if settings.json already exists with agents configured,
// to prevent agents from accidentally wiping a live configuration.
app.post('/api/setup', async (c) => {
const force = c.req.query('force') === 'true';
const settings = (await c.req.json()) as Settings;

// Guard: refuse to overwrite an existing configured installation unless forced
if (!force && fs.existsSync(SETTINGS_FILE)) {
const existing = (() => { try { return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')); } catch { return null; } })();
if (existing?.agents && Object.keys(existing.agents).length > 0) {
log('WARN', '[API] Setup blocked: settings.json already has agents configured. Use ?force=true to overwrite.');
return c.json({ ok: false, error: 'Settings already configured. Pass ?force=true to overwrite.' }, 409);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guard self-documents its own bypass to agents

The 409 error response body explicitly tells callers how to defeat the guard: "Pass ?force=true to overwrite.". An agent that triggers this error will immediately learn the bypass mechanism from the response itself — without needing to read SKILL.md. This substantially undermines the protection for the very scenario the PR is trying to prevent (an agent hallucinating a setup payload and retrying with the hint from the error).

Consider omitting the ?force=true hint from the agent-facing error body and reserving that detail only for server-side logs (which agents typically can't read directly):

Suggested change
return c.json({ ok: false, error: 'Settings already configured. Pass ?force=true to overwrite.' }, 409);
return c.json({ ok: false, error: 'Settings already configured. This endpoint is for initial setup only.' }, 409);

The log message on line 56 already captures the ?force=true detail for human operators, so removing it from the response body doesn't sacrifice operator visibility.

}
}

if (settings.workspace?.path) {
settings.workspace.path = expandHomePath(settings.workspace.path);
}
Expand All @@ -57,8 +69,15 @@ app.post('/api/setup', async (c) => {
}
}

// Write settings.json
// Back up existing settings before overwriting
fs.mkdirSync(path.dirname(SETTINGS_FILE), { recursive: true });
if (fs.existsSync(SETTINGS_FILE)) {
const backupPath = `${SETTINGS_FILE}.bak`;
fs.copyFileSync(SETTINGS_FILE, backupPath);
log('INFO', `[API] Setup: backed up existing settings to ${backupPath}`);
Comment on lines +78 to +80
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single .bak slot — earlier backups silently overwritten

settings.json.bak is a fixed path, so each call with ?force=true overwrites the previous backup. If an operator (or a misbehaving agent) calls POST /api/setup?force=true more than once, all backups except the most recent one are permanently lost. The PR description frames .bak as a recovery mechanism, but it only guarantees recovery from the last overwrite, not from the original state.

A timestamped backup name (e.g., settings.json.bak.<timestamp>) would make recovery more reliable without much added complexity:

Suggested change
const backupPath = `${SETTINGS_FILE}.bak`;
fs.copyFileSync(SETTINGS_FILE, backupPath);
log('INFO', `[API] Setup: backed up existing settings to ${backupPath}`);
const backupPath = `${SETTINGS_FILE}.bak.${Date.now()}`;
fs.copyFileSync(SETTINGS_FILE, backupPath);
log('INFO', `[API] Setup: backed up existing settings to ${backupPath}`);

}

// Write settings.json
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + '\n');
log('INFO', '[API] Setup: settings.json written');

Expand Down