Skip to content

Commit 2a08439

Browse files
committed
v0.8.2: security hardening, thread safety, doc updates
- 🔴 Path traversal guard: ValidateSkillName() rejects /, \, .., hidden prefixes - 🔴 Session file perms: 0644 -> 0600 - 🟠 Thread safety: sync.RWMutex on SkillManager - 🟠 Input size limits: 1MB max on skill body - 🟠 go run/go tool/generate -> code_execution; go install <path> -> install - 🟠 Config: sandbox_volumes now accumulates (not replaces) across layers - 🟡 Dead code: removed unused safePrefixes/safeGoSubcommands vars - 🟡 Stopword dedup: trigger.go now calls shared IsStopword() - 🟡 Init template: skills section included in generated config - 🟡 Renderer: truncate() uses rune count (fixes CJK/emoji breakage) - 📚 README/CLI.md/CONFIG.md: skills system, --learn, import, curate, config ref
1 parent c866a62 commit 2a08439

13 files changed

Lines changed: 261 additions & 72 deletions

File tree

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ kode run "Fix the OOM bug in default-hooks.js"
2121
- **Single binary**`go build` → one file. Drop it anywhere.
2222
- **Multi-turn**`kode run --session` and `kode continue` for persistent conversations
2323
- **Configurable** — 4-layer config (global → project → env → CLI), `${VAR}` substitution
24+
- **Skills system** — On-demand knowledge through trigger‑matched SKILL.md files. Auto‑learn patterns with `--learn`. Import skills from URIs with LLM risk assessment.
2425

2526
## Install
2627

@@ -68,6 +69,9 @@ kode run --model gpt-4o --base-url https://api.openai.com/v1 "Explain this code"
6869

6970
# Sandboxed execution
7071
kode run --sandbox "npm test"
72+
73+
# Enable skill learning (auto-detects patterns, suggests skills)
74+
kode run --learn "Set up a Go project with CI"
7175
```
7276

7377
## Documentation
@@ -80,20 +84,27 @@ kode run --sandbox "npm test"
8084
| **Multi-Turn Sessions** | [docs/SESSIONS.md](docs/SESSIONS.md) — save, continue, list, trim, cleanup |
8185
| **Sandboxing** | [docs/SANDBOXING.md](docs/SANDBOXING.md) — Docker isolation, security, config |
8286
| **Security** | [docs/SECURITY.md](docs/SECURITY.md) — prompt injection, sandbox model |
87+
| **Skills** | [docs/CLI.md#skills](docs/CLI.md#skills) — learn, list, save, import, curate |
8388
| **Development** | [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) — building, testing, contributing |
8489

8590
## Quick reference
8691

8792
```bash
8893
# Commands
8994
kode run [flags] <task> # Single-shot task
95+
kode run --learn [flags] <task> # Run with skill learning
9096
kode run --session [flags] <task> # Save as session
9197
kode continue [--id <id>] <task> # Continue a session
9298
kode session list # List sessions
9399
kode session show [id] # Show session transcript
94100
kode session delete <id> # Delete a session
95101
kode session trim <id> <n> # Keep last n messages
96102
kode session cleanup <days> # Delete old sessions
103+
kode skill list # List available skills
104+
kode skill view <name> # View a skill
105+
kode skill delete <name> # Delete a skill
106+
kode skill import <uri> [--basic --yes] # Import skill from URI
107+
kode skill curate # Quality/overlap audit
97108
kode init [--global] [--force] # Create config file
98109
kode version # Print version
99110

@@ -102,6 +113,7 @@ kode version # Print version
102113
--base-url <url> # API endpoint
103114
--sandbox # Docker sandbox mode
104115
--thinking <level> # enabled/disabled/low/medium/high
116+
--learn # Enable skill learning mode
105117
--system <prompt> # System prompt override
106118
--no-agents # Skip AGENTS.md
107119
```
@@ -123,7 +135,7 @@ result, err := agent.Run(context.Background(), "Summarize this codebase")
123135

124136
```bash
125137
go test ./...
126-
# 108+ tests, all pass, zero external dependencies
138+
# 140+ tests, all pass, zero external dependencies
127139
```
128140

129141
## License

cmd/kode/main.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,21 @@ const defaultConfigTemplate = `{
338338
},
339339
"allowlist": [],
340340
"denylist": []
341+
},
342+
"skills": {
343+
"max_auto_load": 3,
344+
"max_lazy_slots": 5,
345+
"learn": false,
346+
"dirs": [],
347+
"import": {
348+
"max_size_bytes": 1048576,
349+
"timeout_seconds": 5,
350+
"require_https": false
351+
},
352+
"curation": {
353+
"staleness_days": 90,
354+
"auto_prune": false
355+
}
341356
}
342357
}`
343358

docs/CLI.md

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
66
|---------|-------------|
77
| `kode run [flags] <task>` | Execute a task with the agent loop (single-shot by default) |
88
| `kode run --session [flags] <task>` | Execute and save conversation as a multi-turn session |
9+
| `kode run --learn [flags] <task>` | Execute with skill learning (detects patterns, suggests skills) |
910
| `kode continue [--id <id>] <task>` | Continue the most recent session (or by `--id`) |
1011
| `kode repl [--id <id>]` | Interactive REPL mode (persistent multi-turn session) |
1112
| `kode session list` | List sessions |
1213
| `kode session show [id]` | Show session details (default: latest) |
1314
| `kode session delete <id>` | Delete a session |
1415
| `kode session trim <id> <n>` | Keep only the `n` most recent messages |
1516
| `kode session cleanup <days>` | Delete sessions older than N days |
17+
| `kode skill list` | List all available skills |
18+
| `kode skill view <name>` | View a skill's full content |
19+
| `kode skill delete <name>` | Delete a skill |
20+
| `kode skill import <uri> [flags]` | Import a skill from file:// or https:// |
21+
| `kode skill curate` | Analyze skills for quality, staleness, trigger overlap |
1622
| `kode init [--global] [--force]` | Create a config file template |
1723
| `kode version` | Print version and exit |
1824

@@ -28,6 +34,7 @@
2834
| `--no-color` | bool | false | Disable colored terminal output |
2935
| `--no-agents` | bool | false | Skip loading AGENTS.md |
3036
| `--session` | bool | false | Save conversation as a multi-turn session |
37+
| `--learn` | bool | false | Enable skill learning mode (detects patterns, saves skills) |
3138
| `--system <prompt>` | string | built-in | Override system prompt |
3239

3340
## Shell tool schema
@@ -52,8 +59,8 @@ When running without `--sandbox`, kode classifies every shell command by risk an
5259
| 🟠 system_write | **prompt** | `sudo`, `apt install`, writes to `/etc/` |
5360
| 🔴 destructive | **deny** | `rm -rf /`, `dd if=/dev/zero`, `mkfs` |
5461
| 🔴 network_egress | **prompt** | `curl`, `git push`, `ssh`, `scp` |
55-
| 🔴 code_execution | **prompt** | `curl url \| bash`, `eval`, `node -e` |
56-
| 🟠 install | **prompt** | `npm install`, `pip install`, `go install <remote>` |
62+
| 🔴 code_execution | **prompt** | `curl url \| bash`, `eval`, `node -e`, `go run` |
63+
| 🟠 install | **prompt** | `npm install`, `pip install`, `go install <path>` |
5764
| ⬛ blocked | **deny** | Fork bombs, `dd` to block devices |
5865

5966
The approval prompt accepts:
@@ -82,6 +89,87 @@ Configurable via `dangerous` section in `~/kode/config.json` or `./kode.json`:
8289

8390
See [docs/SECURITY.md](docs/SECURITY.md) for details.
8491

92+
## Skills
93+
94+
The **skills system** provides just-in-time domain knowledge to the agent. Skills are SKILL.md files
95+
with YAML frontmatter that define trigger keywords, quality metadata, and markdown body content.
96+
97+
### How skills work
98+
99+
1. Skills are stored in `~/.kode/skills/<name>/SKILL.md` (user-global) or `./.kode/skills/<name>/SKILL.md` (project)
100+
2. Skills with `auto_load: true` are injected into the system prompt on start
101+
3. Lazy skills are loaded on demand when the user's input matches their trigger keywords (topic × action)
102+
4. The `--learn` flag detects reusable patterns during a run and prompts to save as a draft skill
103+
104+
### Skill commands
105+
106+
```bash
107+
# List all skills
108+
kode skill list
109+
110+
# View a skill's full content
111+
kode skill view docker-build
112+
113+
# Delete a skill
114+
kode skill delete docker-build
115+
116+
# Import a skill from a file or URL
117+
kode skill import ./skills/my-skill.md
118+
kode skill import https://example.com/skills/deploy.md
119+
120+
# Import with flags
121+
kode skill import https://example.com/skills/deploy.md --basic # skip LLM risk assessment
122+
kode skill import https://example.com/skills/deploy.md --yes # auto-approve (scripting)
123+
124+
# Run curation (quality, staleness, overlap checks)
125+
kode skill curate
126+
```
127+
128+
### Skill file format
129+
130+
```yaml
131+
---
132+
name: docker-build
133+
description: Build and optimize Docker images
134+
version: 1.0.0
135+
author: kode
136+
kode:
137+
trigger:
138+
topic: docker container image
139+
action: build optimize
140+
auto_load: false
141+
quality: verified
142+
---
143+
## Overview
144+
145+
Procedure for building optimized Docker images.
146+
147+
## Step-by-Step
148+
149+
1. Write a `.dockerignore` file
150+
2. Use multi-stage builds
151+
3. Run `docker build -t <name> .`
152+
153+
## Common Pitfalls
154+
155+
- Forgetting `.dockerignore` leads to large build contexts
156+
- Not pinning base image versions causes build drift
157+
158+
## Verification
159+
160+
- `docker build` exits with code 0
161+
- `docker images` shows the new image
162+
```
163+
164+
### Curation
165+
166+
The `kode skill curate` command runs four quality passes:
167+
168+
- **Staleness** — flags skills unused for 90+ days (configurable via `skill.curation.staleness_days`)
169+
- **Trigger overlap** — detects skills with 2+ shared topic keywords that may need merging
170+
- **Quality audit** — checks for missing sections, short bodies, long descriptions
171+
- **Body dedup** — detects skills with identical body content by SHA256 hash
172+
85173
## Sandbox flags
86174

87175
| Flag | Default | Description |
@@ -144,6 +232,9 @@ kode run --sandbox --sandbox-image node:20-alpine "node --version"
144232
145233
# Custom system prompt
146234
kode run --system "You are a Go expert. Answer with code only." "Write HTTP server"
235+
236+
# Run with skill learning
237+
kode run --learn "Set up CI with GitHub Actions"
147238
```
148239

149240
## Config priority

docs/CONFIG.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Every config knob has a `KODE_*` counterpart:
6262
| `KODE_NO_COLOR` | `--no-color` | bool |
6363
| `KODE_NO_AGENTS` | `--no-agents` | bool |
6464
| `KODE_SYSTEM` | `--system` | string |
65+
| `KODE_SKILLS_LEARN` | `skills.learn` | bool |
6566
| `KODE_SANDBOX_IMAGE` | `--sandbox-image` | string |
6667
| `KODE_SANDBOX_NETWORK` | `--sandbox-network` | string |
6768
| `KODE_SANDBOX_READONLY` | `--sandbox-readonly` | bool |
@@ -73,6 +74,42 @@ Every config knob has a `KODE_*` counterpart:
7374

7475
`KODE_API_KEY``DEEPSEEK_API_KEY``OPENAI_API_KEY`
7576

77+
## Skills configuration
78+
79+
The `skills` section controls the skill system:
80+
81+
```json
82+
{
83+
"skills": {
84+
"max_auto_load": 3,
85+
"max_lazy_slots": 5,
86+
"learn": false,
87+
"dirs": [],
88+
"import": {
89+
"max_size_bytes": 1048576,
90+
"timeout_seconds": 5,
91+
"require_https": false
92+
},
93+
"curation": {
94+
"staleness_days": 90,
95+
"auto_prune": false
96+
}
97+
}
98+
}
99+
```
100+
101+
| Field | Env var | Default | Description |
102+
|-------|---------|---------|-------------|
103+
| `max_auto_load` || 3 | Max skills injected into system prompt on start |
104+
| `max_lazy_slots` || 5 | Max skills loaded per user input via trigger matching |
105+
| `learn` | `KODE_SKILLS_LEARN` | false | Enable skill learning mode (detects patterns, suggests skills) |
106+
| `dirs` || [] | Extra skill directories beyond `~/.kode/skills` and `./.kode/skills` |
107+
| `import.max_size_bytes` || 1048576 (1MB) | Max size for fetched skill content |
108+
| `import.timeout_seconds` || 5 | HTTP timeout for skill URI fetch |
109+
| `import.require_https` || false | Reject http:// URIs when true |
110+
| `curation.staleness_days` || 90 | Days without use before flagging as stale |
111+
| `curation.auto_prune` || false | Auto-delete stale skills on curate (no prompt) |
112+
76113
## kode init
77114

78115
Create a config file template:
@@ -102,6 +139,9 @@ kode run "quick status"
102139
# Env var override for one-off
103140
KODE_SANDBOX=true kode run "run untrusted script"
104141

142+
# Enable skill learning via env var
143+
KODE_SKILLS_LEARN=true kode run "set up CI"
144+
105145
# CLI flag always wins
106146
kode run --model gpt-4o --base-url https://api.openai.com/v1 "task"
107147
```

internal/config/loader.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ package config
1313

1414
import (
1515
"encoding/json"
16+
"fmt"
1617
"os"
1718
"path/filepath"
1819
"strconv"
@@ -202,7 +203,8 @@ func loadFile(path string) FileConfig {
202203
}
203204
var cfg FileConfig
204205
if err := json.Unmarshal(data, &cfg); err != nil {
205-
return FileConfig{} // invalid JSON = empty (silent)
206+
fmt.Fprintf(os.Stderr, "kode: warning: config %s: invalid JSON — ignoring file: %v\n", path, err)
207+
return FileConfig{} // invalid JSON = empty
206208
}
207209
// Expand environment variables in all string fields
208210
cfg.Model = expandEnv(cfg.Model)
@@ -536,7 +538,7 @@ func overlayFile(base, override FileConfig) FileConfig {
536538
}
537539
}
538540
if override.SandboxVolumes != nil {
539-
base.SandboxVolumes = append([]string{}, override.SandboxVolumes...)
541+
base.SandboxVolumes = append(base.SandboxVolumes, override.SandboxVolumes...)
540542
}
541543
if override.Dangerous != nil {
542544
base.Dangerous = override.Dangerous

internal/danger/classifier.go

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -266,20 +266,8 @@ func tokenize(input string) []string {
266266
}
267267

268268
// ── Safe command prefixes ──────────────────────────────────────────────
269-
270-
var safePrefixes = map[string]bool{
271-
"ls": true, "cat": true, "head": true, "tail": true,
272-
"less": true, "more": true, "pwd": true, "which": true,
273-
"find": true, "grep": true, "egrep": true, "fgrep": true,
274-
"wc": true, "sort": true, "uniq": true, "diff": true, "cmp": true,
275-
"date": true, "env": true, "printenv": true, "cd": true,
276-
"go": true, // prefix-checked below for subcommands
277-
}
278-
279-
// safeGoSubcommands are go subcommands with no side effects.
280-
var safeGoSubcommands = map[string]bool{
281-
"build": true, "vet": true, "fmt": true, "mod": true, "test": true,
282-
}
269+
// (Unused — classification falls through to Safe by default. Kept as
270+
// documentation of what's considered read-only.)
283271

284272
var writePrefixes = map[string]bool{
285273
"echo": true, "sed": true, "awk": true, "tee": true,
@@ -597,6 +585,14 @@ func isCodeExecution(first string, tokens []string) bool {
597585
}
598586

599587
if !codeEvalPrefixes[first] {
588+
// Check go run / go tool — compiles and executes code
589+
if first == "go" {
590+
for _, tok := range tokens[1:] {
591+
if tok == "run" || tok == "tool" || tok == "generate" {
592+
return true
593+
}
594+
}
595+
}
600596
return false
601597
}
602598

@@ -634,21 +630,19 @@ func isInstall(first string, tokens []string) bool {
634630
return hasArgAfter(tokens, "cargo", "install")
635631
}
636632

637-
// go install <remote>
633+
// go install <remote> OR go install <local-path>
638634
if first == "go" {
639-
// go install with no args = local build (safe)
640-
// go install <path>@<version> = remote install
641635
hasInstall := false
642636
for _, tok := range tokens[1:] {
643637
if tok == "install" {
644638
hasInstall = true
645639
continue
646640
}
647-
if hasInstall && strings.Contains(tok, "@") {
648-
return true
641+
if hasInstall {
642+
return true // go install <something> downloads deps
649643
}
650644
}
651-
return false
645+
return false // bare "go install" = local build only
652646
}
653647

654648
// brew install

internal/render/render.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"os"
2323
"strings"
2424
"time"
25+
"unicode/utf8"
2526
)
2627

2728
// ── Events ────────────────────────────────────────────────────────────
@@ -222,12 +223,14 @@ func (r *Renderer) style(code, text string) string {
222223
return code + text + reset
223224
}
224225

225-
// truncate limits s to n chars, adding "…" if truncated.
226+
// truncate limits s to n characters (not bytes), adding "…" if truncated.
226227
func (r *Renderer) truncate(s string, n int) string {
227-
if len(s) <= n {
228+
if utf8.RuneCountInString(s) <= n {
228229
return s
229230
}
230-
return s[:n] + "…"
231+
// Convert to runes, truncate, then convert back
232+
runes := []rune(s)
233+
return string(runes[:n]) + "…"
231234
}
232235

233236
// ── Auto-detection ────────────────────────────────────────────────────

0 commit comments

Comments
 (0)