44 "context"
55 "crypto/sha256"
66 "encoding/hex"
7+ "encoding/json"
78 "fmt"
89 "os"
910 "os/exec"
@@ -71,6 +72,11 @@ Tool output handling:
7172// Docker image from it. See buildFromDockerfile() and SANDBOXING.md.
7273const dockerfileName = "Dockerfile.kode"
7374
75+ // forbiddenMountPrefixes lists host paths that sandbox volume mounts
76+ // may not target. Mounting /, /etc, /proc, /sys would give the sandbox
77+ // container access to the host filesystem, defeating isolation.
78+ var forbiddenMountPrefixes = []string {"/" , "/etc" , "/proc" , "/sys" , "/boot" , "/dev" }
79+
7480func boolPtr (b bool ) * bool { return & b }
7581
7682func main () {
@@ -225,13 +231,93 @@ done:
225231 return f , nil
226232}
227233
234+ // ── REPL Flag Parsing ──────────────────────────────────────────────────
235+
236+ // replFlags holds the parsed CLI flags for `kode repl`.
237+ // Same resolution model as runFlags: zero/nil = not set,
238+ // config loader merges file → env → CLI.
239+ type replFlags struct {
240+ ID string // session ID to resume
241+ Model string
242+ Thinking string
243+ Sandbox * bool // nil = not set
244+
245+ // Sandbox-specific CLI flags
246+ SandboxImage string
247+ SandboxNetwork string
248+ SandboxReadonly * bool
249+ SandboxMemory string
250+ SandboxCPUs string
251+ SandboxUser string
252+ }
253+
254+ // parseReplFlags parses `kode repl` arguments and returns the parsed flags.
255+ // Exported for testing. Unlike parseRunFlags, there is no required task argument;
256+ // unrecognized flags or trailing args are silently ignored.
257+ func parseReplFlags (args []string ) (replFlags , error ) {
258+ var f replFlags
259+ if len (args ) == 0 {
260+ return f , nil
261+ }
262+
263+ i := 0
264+ for i < len (args ) {
265+ if i == len (args )- 1 {
266+ // Last arg — can only be a boolean flag (no value pair needed)
267+ switch args [i ] {
268+ case "--sandbox" :
269+ f .Sandbox = boolPtr (true )
270+ case "--sandbox-readonly" :
271+ f .SandboxReadonly = boolPtr (true )
272+ }
273+ break
274+ }
275+ switch args [i ] {
276+ case "--id" :
277+ f .ID = args [i + 1 ]
278+ i += 2
279+ case "--model" :
280+ f .Model = args [i + 1 ]
281+ i += 2
282+ case "--thinking" :
283+ f .Thinking = args [i + 1 ]
284+ i += 2
285+ case "--sandbox" :
286+ f .Sandbox = boolPtr (true )
287+ i ++
288+ case "--sandbox-image" :
289+ f .SandboxImage = args [i + 1 ]
290+ i += 2
291+ case "--sandbox-network" :
292+ f .SandboxNetwork = args [i + 1 ]
293+ i += 2
294+ case "--sandbox-readonly" :
295+ f .SandboxReadonly = boolPtr (true )
296+ i ++
297+ case "--sandbox-memory" :
298+ f .SandboxMemory = args [i + 1 ]
299+ i += 2
300+ case "--sandbox-cpus" :
301+ f .SandboxCPUs = args [i + 1 ]
302+ i += 2
303+ case "--sandbox-user" :
304+ f .SandboxUser = args [i + 1 ]
305+ i += 2
306+ default :
307+ // Unrecognized flag or positional — skip it
308+ i ++
309+ }
310+ }
311+ return f , nil
312+ }
313+
228314func printUsage () {
229315 fmt .Println (`Usage:
230316 kode run [flags] <task>
231317 kode run --session [flags] <task>
232318 kode continue [--id <id>] <task>
233319 kode session <list|show [id]|trim <id> <n>|delete <id>|cleanup <days>>
234- kode repl [--id <id> ]
320+ kode repl [flags ]
235321 kode init [--global | -g] [--force | -f]
236322 kode version
237323
@@ -241,6 +327,8 @@ Commands:
241327 run --session Execute and save conversation as a session
242328 continue Continue the most recent session (or by --id)
243329 repl Interactive REPL mode (multi-turn session)
330+ Accepts --model, --thinking, --sandbox, and
331+ --sandbox-* flags just like kode run.
244332 session Manage sessions: list, show, delete, trim, cleanup
245333 skill Manage skills: list, view, save, delete, import, curate
246334 init Create a config file (default: ./kode.json)
@@ -406,7 +494,7 @@ func initConfig(args []string) error {
406494 return fmt .Errorf ("create directory %s: %w" , dir , err )
407495 }
408496
409- if err := os .WriteFile (configPath , []byte (defaultConfigTemplate + "\n " ), 0644 ); err != nil {
497+ if err := os .WriteFile (configPath , []byte (defaultConfigTemplate + "\n " ), 0600 ); err != nil {
410498 return fmt .Errorf ("write config: %w" , err )
411499 }
412500
@@ -789,7 +877,22 @@ func buildSandboxArgs(cfg sandboxConfig, containerName, workdir, image string) [
789877
790878 // Extra volume mounts
791879 for _ , vol := range cfg .Volumes {
792- args = append (args , "-v" , vol )
880+ // Validate: reject mounts to sensitive host paths
881+ reject := false
882+ parts := strings .SplitN (vol , ":" , 2 )
883+ if len (parts ) > 0 {
884+ hostPath := filepath .Clean (parts [0 ])
885+ for _ , forbidden := range forbiddenMountPrefixes {
886+ if hostPath == forbidden || strings .HasPrefix (hostPath , forbidden + "/" ) {
887+ fmt .Fprintf (os .Stderr , "kode: WARNING: rejecting forbidden volume mount %q (host path %s)\n " , vol , hostPath )
888+ reject = true
889+ break
890+ }
891+ }
892+ }
893+ if ! reject {
894+ args = append (args , "-v" , vol )
895+ }
793896 }
794897
795898 // Image and command
@@ -951,7 +1054,7 @@ func skillCmd(args []string) error {
9511054 sm := skills .NewSkillManager (userDir , "./.kode/skills" )
9521055 tool := & skills.SkillLoadTool {}
9531056 tool .Manager = sm
954- result , err := tool .Call (`{"name": "` + subArgs [0 ] + `"}` )
1057+ result , err := tool .Call (jsonMarshalName ( subArgs [0 ]) )
9551058 if err != nil {
9561059 return err
9571060 }
@@ -965,7 +1068,7 @@ func skillCmd(args []string) error {
9651068 sm := skills .NewSkillManager (userDir , "./.kode/skills" )
9661069 tool := & skills.SkillDeleteTool {}
9671070 tool .Manager = sm
968- result , err := tool .Call (`{"name": "` + subArgs [0 ] + `"}` )
1071+ result , err := tool .Call (jsonMarshalName ( subArgs [0 ]) )
9691072 if err != nil {
9701073 return err
9711074 }
@@ -988,12 +1091,13 @@ func skillCmd(args []string) error {
9881091 }
9891092 }
9901093
1094+ // Load config once for both RequireHTTPS and LLM assessment
1095+ cfg := config .LoadConfig (config.CLIFlags {})
1096+
9911097 llmCall := func (prompt string ) (string , error ) {
9921098 if basicOnly {
9931099 return "" , fmt .Errorf ("basic mode — no LLM call" )
9941100 }
995- // Load config and create LLM client for assessment
996- cfg := config .LoadConfig (config.CLIFlags {})
9971101 client := llm .New (cfg .BaseURL , cfg .APIKey , cfg .Model , "" , 30 )
9981102 ctx , cancel := context .WithTimeout (context .Background (), 30 * time .Second )
9991103 defer cancel ()
@@ -1004,12 +1108,13 @@ func skillCmd(args []string) error {
10041108 }
10051109
10061110 result , err := skills .ImportSkill (skills.ImportOptions {
1007- URI : uri ,
1008- MaxBytes : 1_048_576 ,
1009- Timeout : 5 ,
1010- BasicOnly : basicOnly ,
1011- AutoYes : autoYes ,
1012- UserDir : userDir ,
1111+ URI : uri ,
1112+ MaxBytes : 1_048_576 ,
1113+ Timeout : 5 ,
1114+ BasicOnly : basicOnly ,
1115+ AutoYes : autoYes ,
1116+ RequireHTTPS : cfg .Skills .Import .RequireHTTPS ,
1117+ UserDir : userDir ,
10131118 }, func (assessment * skills.ImportAssessment ) bool {
10141119 if autoYes {
10151120 return true
@@ -1424,3 +1529,15 @@ func shorten(s string, n int) string {
14241529 }
14251530 return s [:n ] + "…"
14261531}
1532+
1533+ // ── JSON Injection Prevention ─────────────────────────────────────────
1534+
1535+ // jsonMarshalName safely marshals a skill name into a JSON object
1536+ // {"name":"<escaped>"}. Uses json.Marshal to prevent JSON injection
1537+ // from names containing quotes, backslashes, or control characters.
1538+ func jsonMarshalName (name string ) string {
1539+ b , _ := json .Marshal (struct {
1540+ Name string `json:"name"`
1541+ }{Name : name })
1542+ return string (b )
1543+ }
0 commit comments