Skip to content

fix(settings): guard POST /api/setup against agent overwrites#222

Open
jcenters wants to merge 3 commits intoTinyAGI:mainfrom
jcenters:fix/protect-settings-from-agent-overwrite
Open

fix(settings): guard POST /api/setup against agent overwrites#222
jcenters wants to merge 3 commits intoTinyAGI:mainfrom
jcenters:fix/protect-settings-from-agent-overwrite

Conversation

@jcenters
Copy link

Problem

POST /api/setup does a full replacement of settings.json — not a merge. Agents running with --dangerously-skip-permissions can call this endpoint (the tinyclaw-admin skill documents it) and silently wipe a live configuration.

This is reproducible in practice: agents with a heartbeat or a poorly-scoped task will invoke the admin skill, hallucinate a setup payload, call the endpoint, and replace the real config with partial or empty JSON. On the next restart, TinyClaw comes up with no agents and re-runs setup.

Changes

packages/server/src/routes/settings.ts

  • POST /api/setup now returns HTTP 409 if settings.json already has agents configured, unless ?force=true is passed
  • Creates a .bak before any overwrite, so recovery is possible if ?force=true is intentionally used

.agents/skills/tinyclaw-admin/SKILL.md

  • Adds a prominent warning that agents must not edit settings.json directly or call POST /api/setup on a running system
  • Explains that POST /api/setup is initial-setup-only and will be rejected on configured systems
  • Notes that direct file editing is a last resort for human operators only

Test plan

  • Fresh install: POST /api/setup with valid settings succeeds (no agents in file yet)
  • Configured install: POST /api/setup returns 409, settings.json unchanged
  • Configured install + ?force=true: overwrites settings.json, creates .bak
  • PUT /api/settings (shallow merge) still works normally

🤖 Generated with Claude Code

POST /api/setup does a full replacement of settings.json, not a merge.
Agents with dangerously-skip-permissions can call this endpoint and
silently wipe a live configuration — which is exactly what was happening
in the wild.

Two changes:
- Server: POST /api/setup now returns HTTP 409 if settings.json already
  has agents configured, unless ?force=true is passed. Also creates a
  .bak before any overwrite so recovery is possible.
- Skill: tinyclaw-admin SKILL.md now warns agents away from both direct
  file editing and the /api/setup endpoint, and explains why.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR adds a guard to POST /api/setup that returns HTTP 409 if settings.json already has agents configured, plus a .bak backup before any forced overwrite, and updates SKILL.md with warnings for agents. The fix addresses a real and reproducible problem — but two aspects of the implementation risk undermining the protection it introduces.

Key changes:

  • POST /api/setup now checks for existing agents and rejects with 409 unless ?force=true is passed
  • A settings.json.bak file is written before any overwrite when ?force=true is used
  • SKILL.md adds a prominent WARNING block discouraging agents from direct settings.json edits

Issues found:

  • The 409 error response body ("Pass ?force=true to overwrite.") self-documents the bypass to any agent that hits the guard — the very agents the PR is protecting against will learn the workaround from the response itself
  • SKILL.md similarly teaches agents the ?force=true mechanism in agent-readable context, compounding the discoverability issue
  • The .bak backup uses a fixed path (settings.json.bak), so repeated ?force=true calls silently overwrite the previous backup — only single-step rollback is possible

Confidence Score: 3/5

  • Safe to merge as an improvement over the status quo, but the guard can be self-taught to agents via the 409 response body and SKILL.md, limiting its effectiveness.
  • The guard is a net positive — it stops naive agent overwrites. However, the 409 response body and the SKILL.md update both explicitly document ?force=true to agent-readable surfaces, which means an agent that encounters a 409 or reads the skill file learns the bypass. The backup mechanism also only provides single-step recovery. These are real gaps that should be addressed before the protection can be considered robust.
  • Both changed files need attention: the error message in packages/server/src/routes/settings.ts line 57 should not advertise the bypass, and the ?force=true documentation in .agents/skills/tinyclaw-admin/SKILL.md line 205 should be removed from agent-readable context.

Important Files Changed

Filename Overview
packages/server/src/routes/settings.ts Adds a 409 guard and backup logic to POST /api/setup. The guard is logically sound but the 409 error response body self-documents the ?force=true bypass, and the single .bak slot provides only single-step rollback.
.agents/skills/tinyclaw-admin/SKILL.md Adds a clear WARNING discouraging agents from editing settings.json directly, and documents the POST /api/setup 409 behavior — but also explicitly documents the ?force=true bypass in agent-readable context, which undermines the protection it describes.

Sequence Diagram

sequenceDiagram
    participant Agent
    participant API as POST /api/setup
    participant FS as settings.json

    Agent->>API: POST /api/setup (no ?force)
    API->>FS: existsSync(SETTINGS_FILE)?
    alt File exists with agents
        FS-->>API: agents: { ... }
        API-->>Agent: 409 — "Pass ?force=true to overwrite" ⚠️ bypass hint exposed
    else File missing or no agents
        API->>FS: mkdirSync + writeFileSync(settings)
        FS-->>API: written
        API-->>Agent: 200 ok
    end

    Agent->>API: POST /api/setup?force=true
    API->>FS: existsSync? → copyFileSync → settings.json.bak (single slot, overwritten each time)
    API->>FS: writeFileSync(settings)
    FS-->>API: written
    API-->>Agent: 200 ok
Loading

Last reviewed commit: c4bfc58

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

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.

Comment on lines +75 to +77
const backupPath = `${SETTINGS_FILE}.bak`;
fs.copyFileSync(SETTINGS_FILE, backupPath);
log('INFO', `[API] Setup: backed up existing settings to ${backupPath}`);
Copy link

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}`);


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

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.

jcenters and others added 2 commits March 16, 2026 02:14
ensure_agent_skills_links() was unconditionally deleting and re-copying
every built-in skill directory on every daemon start. Any skill created
by an agent (not present in the source) was silently destroyed on restart.

Replace the copy strategy with per-skill symlinks pointing at the source
directory. This means:
- Built-in skills stay current automatically (symlink always points to source)
- Agent-created skills (real directories, not symlinks) are left untouched
- Stale symlinks for skills removed from source are cleaned up

Also converted existing copied skill directories in all agent workspaces
to symlinks so the fix takes effect without requiring a clean reinstall.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous guard allowed ?force=true to skip the 409 check entirely.
Agents can discover and exploit query parameters — the tinyclaw-admin
skill documents the API in detail — so a bypassable guard is no better
than no guard at runtime.

Remove the force parameter entirely. The endpoint now unconditionally
rejects setup attempts on a configured system. To modify a running
installation, use PUT /api/agents/:id, PUT /api/teams/:id, or
PUT /api/settings. Updated SKILL.md to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant