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
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. There is no override. To modify a running system, use `PUT /api/agents/:id`, `PUT /api/teams/:id`, or `PUT /api/settings`.

## Modifying TinyClaw source code

When modifying TinyClaw's own code (features, bug fixes, new routes, etc.):
Expand Down
40 changes: 35 additions & 5 deletions lib/daemon.sh
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,14 @@ status_daemon() {

# --- Agent skills management (called by start_daemon) ---

# Ensure all agent workspaces have .agents/skills synced from SCRIPT_DIR
# and .claude/skills as a symlink to .agents/skills
# Ensure all agent workspaces have .agents/skills populated from SCRIPT_DIR
# and .claude/skills as a symlink to .agents/skills.
#
# Each built-in skill is installed as a symlink into the agent's .agents/skills/
# directory rather than a copy. This means:
# - Built-in skills stay current without wiping agent-created skills.
# - Skills created by agents (not in the source) are left untouched.
# - Stale symlinks (skill removed from source) are cleaned up automatically.
ensure_agent_skills_links() {
local skills_src="$SCRIPT_DIR/.agents/skills"
[ -d "$skills_src" ] || return 0
Expand All @@ -389,14 +395,38 @@ ensure_agent_skills_links() {
local agent_dir="$agents_dir/$agent_id"
[ -d "$agent_dir" ] || continue

# Sync default skills into .agents/skills
mkdir -p "$agent_dir/.agents/skills"

# Install each built-in skill as a symlink (absolute path).
# If a real directory exists with the same name (agent-created copy),
# leave it alone — don't overwrite agent work.
for skill_dir in "$skills_src"/*/; do
[ -d "$skill_dir" ] || continue
local skill_name
skill_name="$(basename "$skill_dir")"
rm -rf "$agent_dir/.agents/skills/$skill_name"
cp -r "$skill_dir" "$agent_dir/.agents/skills/$skill_name"
local dest="$agent_dir/.agents/skills/$skill_name"

if [ -L "$dest" ]; then
# Already a symlink — update it to point to current source
ln -sfn "$skill_dir" "$dest"
elif [ -d "$dest" ]; then
# Real directory: agent-created skill, leave it alone
:
else
# Not present — create symlink
ln -s "$skill_dir" "$dest"
fi
done

# Remove symlinks that point to skills no longer in source
# (built-in skill was deleted upstream). Real directories are kept.
for dest in "$agent_dir/.agents/skills"/*/; do
[ -L "${dest%/}" ] || continue
local skill_name
skill_name="$(basename "$dest")"
if [ ! -d "$skills_src/$skill_name" ]; then
rm "$agent_dir/.agents/skills/$skill_name"
fi
done

# Ensure .claude/skills is a symlink to ../.agents/skills
Expand Down
24 changes: 23 additions & 1 deletion packages/server/src/routes/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,24 @@ app.put('/api/settings', async (c) => {
});

// POST /api/setup — run initial setup (write settings + create directories)
// Blocked if settings.json already exists with agents configured.
// To reconfigure a live system, use PUT /api/agents/:id, PUT /api/teams/:id,
// or PUT /api/settings instead.
app.post('/api/setup', async (c) => {
const settings = (await c.req.json()) as Settings;

// Guard: refuse to overwrite an existing configured installation.
// There is intentionally no bypass parameter — agents can discover and
// exploit query parameters. Use the individual PUT endpoints to modify
// a running configuration.
if (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.');
return c.json({ ok: false, error: 'Settings already configured. Use PUT /api/agents, /api/teams, or /api/settings to modify a running system.' }, 409);
}
}

if (settings.workspace?.path) {
settings.workspace.path = expandHomePath(settings.workspace.path);
}
Expand All @@ -57,8 +72,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