package main // The command line a script written for Claude Code's `claude` sends. // // `claude -p` is how a lot of CI jobs and shell scripts run an agent, and the // point of this file is that pointing one of them at Codey instead should be a // matter of changing the binary's name. So the flags those scripts pass are // read here, in Claude Code's spelling and with Claude Code's meaning, and // turned into the settings Codey already had: --dangerously-skip-permissions // is -yolo, --continue is -resume latest, --allowedTools is a list of allow // rules on the approval policy. // // Two of them do not fit Go's flag package as they are. `claude -p` is a // switch and the prompt is a positional argument — `claude -p --output-format // json "fix it"` — where Codey's -p takes the prompt as its value; and // --allowedTools takes several values in a row. prepareArgs reads the line // first and hands the flag package something it can parse, so that both // `codey -p "fix it"` and the Claude Code spelling work. import ( "errors" "flag" "fmt" "io" "os" "strings" "golang.org/x/term" "github.com/johannSo/codey/internal/approval" ) // Output formats `-p` can print in. The empty format is Codey's own: the // streamed transcript and a usage line, for a person reading along. const ( formatText = "text" formatJSON = "json" formatStreamJSON = "stream-json" ) // claudeFlags are the flags `claude` takes that Codey answers to as well. type claudeFlags struct { print bool outputFormat string inputFormat string verbose bool cont bool allowed ruleList disallowed ruleList appendSystem string systemPrompt string permissionMode string skipPermissions bool partial bool } // ruleList collects permission rules from every time the flag is given, each // time a list of its own: --allowedTools "Read,Edit" --allowedTools "Bash(go test:*)". type ruleList []string func (l *ruleList) String() string { return strings.Join(*l, ",") } func (l *ruleList) Set(v string) error { *l = append(*l, approval.SplitRules(v)...) return nil } // ruleFlags are the flags prepareArgs lets take several values in a row. var ruleFlags = map[string]bool{ "allowedTools": true, "allowed-tools": true, "disallowedTools": true, "disallowed-tools": true, } func registerClaudeFlags(fs *flag.FlagSet) *claudeFlags { c := &claudeFlags{} fs.BoolVar(&c.print, "print", false, "the same as -p; the prompt may follow it, or come after the other flags, or on stdin") fs.StringVar(&c.outputFormat, "output-format", "", "with -p: text (the answer only), json (one result object) or stream-json (one line per message), in the shapes claude -p prints") fs.StringVar(&c.inputFormat, "input-format", formatText, "with -p: only text is supported") fs.BoolVar(&c.verbose, "verbose", false, "the same as -v") fs.BoolVar(&c.cont, "c", false, "continue the most recent session in this working root, the same as -resume latest") fs.BoolVar(&c.cont, "continue", false, "the same as -c") ruleHelp := "comma- or space-separated rules in Claude Code's syntax: Bash, Bash(git diff:*), Read(./src/**), mcp__server" fs.Var(&c.allowed, "allowedTools", "tool calls to approve without asking; "+ruleHelp) fs.Var(&c.allowed, "allowed-tools", "the same as -allowedTools") fs.Var(&c.disallowed, "disallowedTools", "tools to take away, or calls to refuse whatever else says yes; "+ruleHelp) fs.Var(&c.disallowed, "disallowed-tools", "the same as -disallowedTools") fs.StringVar(&c.appendSystem, "append-system-prompt", "", "text added to the end of the system prompt") fs.StringVar(&c.systemPrompt, "system-prompt", "", "replace Codey's system prompt with this text") fs.StringVar(&c.permissionMode, "permission-mode", "", "default, acceptEdits (edits without asking), bypassPermissions (the same as -yolo) or dontAsk (refuse what no rule allows)") fs.BoolVar(&c.skipPermissions, "dangerously-skip-permissions", false, "the same as -yolo") fs.BoolVar(&c.partial, "include-partial-messages", false, "accepted for compatibility and ignored: stream-json prints whole messages only") return c } // prepareArgs rewrites the command line into one the flag package reads the // way `claude` would have. // // Go's flag parsing stops at the first argument that is not a flag, and // commander, which `claude` uses, does not: `claude "fix it" --model x` is a // prompt and a model. So the flags are pulled out wherever they are, and the // positional arguments go after a "--" at the end, where flag.Args finds them. // Knowing which flags take a value is what makes that possible, and the flag // set already knows. // // -p and --print take the next argument as the prompt when it is not a flag, // which is both Codey's -p "prompt" and Claude Code's -p "prompt"; followed by // a flag or by nothing, they are a switch, and the prompt is whatever // positional arguments there are, or stdin. The rule flags take every // following argument that reads as a rule, as commander's variadic options do // — except that "reads as a rule" here means it parses as one, so a one-word // prompt after --allowedTools Read is still a prompt. // // Anything it does not recognise is passed through untouched, for the flag // package to report in its own words. func prepareArgs(fs *flag.FlagSet, args []string, c *claudeFlags) []string { var flags, positional []string for i := 0; i < len(args); i++ { a := args[i] if a == "--" { positional = append(positional, args[i+1:]...) break } if !isFlag(a) { positional = append(positional, a) continue } name, _, hasValue := strings.Cut(strings.TrimLeft(a, "-"), "=") if (name == "p" || name == "print") && !hasValue { if i+1 < len(args) && !isFlag(args[i+1]) { flags = append(flags, "-p", args[i+1]) i++ continue } c.print = true continue } f := fs.Lookup(name) if f == nil || hasValue || isBoolFlag(f) || i+1 >= len(args) { flags = append(flags, a) continue } flags = append(flags, a, args[i+1]) i++ if ruleFlags[name] { for i+1 < len(args) && !isFlag(args[i+1]) && readsAsRules(args[i+1]) { flags = append(flags, a, args[i+1]) i++ } } } return append(append(flags, "--"), positional...) } // isFlag is the flag package's own test: a dash and something after it. func isFlag(a string) bool { return len(a) > 1 && a[0] == '-' } func isBoolFlag(f *flag.Flag) bool { b, ok := f.Value.(interface{ IsBoolFlag() bool }) return ok && b.IsBoolFlag() } // readsAsRules reports whether every item in an argument parses as a rule. func readsAsRules(a string) bool { items := approval.SplitRules(a) if len(items) == 0 { return false } for _, s := range items { if _, err := approval.ParseRule(s); err != nil { return false } } return true } // cliRun is what the Claude Code flags came to. type cliRun struct { // Prompt is the -p prompt, put together from the flag, the positional // arguments and stdin; empty for an interactive session. Prompt string // FromStdin is set when the prompt was read from stdin, which leaves // nothing there to read an approval from. FromStdin bool // Format is the -p output format, empty for Codey's own. Format string // Resume is -resume, or "latest" for --continue. Resume string // YOLO is --dangerously-skip-permissions or bypassPermissions. YOLO bool // NoPrompt refuses whatever no rule allows rather than asking. NoPrompt bool // Mode is the permission mode, in Claude Code's words, for the init line. Mode string // Rules go on the approval policy; Drop is the part of them that takes // whole tools away, for the registry; and DropHostedSearch is the same // for the one tool the registry never holds. Rules approval.RuleList Drop func(name string) bool DropHostedSearch bool // System replaces the system prompt when set, and SystemAppend goes on // the end of it. System, SystemAppend string // Warnings are for the user, once there is somewhere to say them. Warnings []string } // UIOut is where the line front end may write its prompts. In a Claude-shaped // run stdout belongs to the output format, so a question goes to stderr. func (r cliRun) UIOut() io.Writer { if r.Format != "" { return os.Stderr } return os.Stdout } // resolve turns the parsed flags into a cliRun. prompt and resume are Codey's // own -p and -resume, positional is what flag parsing left over, and root is // what relative path rules are read against. func (c *claudeFlags) resolve(prompt string, positional []string, resume, root string, stdin *os.File) (cliRun, error) { r := cliRun{Resume: resume, System: c.systemPrompt, SystemAppend: c.appendSystem, Mode: "default"} printing := c.print || prompt != "" switch c.outputFormat { case "", formatText, formatJSON, formatStreamJSON: default: return r, fmt.Errorf("--output-format %q: want text, json or stream-json", c.outputFormat) } if c.outputFormat != "" && !printing { return r, errors.New("--output-format applies only with -p") } r.Format = c.outputFormat if c.inputFormat != formatText { return r, fmt.Errorf("--input-format %q is not supported: the prompt is read as text", c.inputFormat) } if c.partial { r.Warnings = append(r.Warnings, "--include-partial-messages is ignored: stream-json prints whole messages only") } if c.cont { if resume != "" && resume != "latest" { return r, fmt.Errorf("--continue and -resume %s ask for different sessions; give one", resume) } r.Resume = "latest" } if printing { parts := positional if prompt != "" { parts = append([]string{prompt}, positional...) } r.Prompt = strings.TrimSpace(strings.Join(parts, " ")) if r.Prompt == "" && !isTerminal(stdin) { // What Claude Code does with `echo "fix it" | claude -p`. Stdin // is read only when there is no prompt otherwise: a CI runner can // leave it as a pipe nobody ever closes, and a run that waited on // it would hang there for good. b, err := io.ReadAll(stdin) if err != nil { return r, fmt.Errorf("reading the prompt from stdin: %w", err) } r.Prompt, r.FromStdin = strings.TrimSpace(string(b)), true } if r.Prompt == "" { return r, errors.New("-p needs a prompt: give it after -p, as an argument, or on stdin") } } else if len(positional) > 0 { r.Warnings = append(r.Warnings, fmt.Sprintf("ignoring %q: a prompt on the command line needs -p", strings.Join(positional, " "))) } // With nobody at a terminal there is nobody to ask, and a Claude-shaped // run never asks anyway: what no rule allows is refused, and the model // is told so. Codey's own -p keeps reading answers from stdin, which is // how `yes | codey -p ...` has always worked. r.NoPrompt = r.FromStdin || (r.Format != "" && !isTerminal(stdin)) allow, errs := approval.ParseRules(c.allowed) deny, denyErrs := approval.ParseRules(c.disallowed) for _, err := range append(errs, denyErrs...) { r.Warnings = append(r.Warnings, fmt.Sprintf("%v — ignored", err)) } r.YOLO = c.skipPermissions switch c.permissionMode { case "", "default": case "acceptEdits": // The mode the ACP front end calls by the same name: edits go // through, commands still ask. An Edit rule says exactly that. edit, _ := approval.ParseRule("Edit") allow = append(allow, edit) r.Mode = c.permissionMode case "bypassPermissions": r.YOLO, r.Mode = true, c.permissionMode case "dontAsk": r.NoPrompt, r.Mode = true, c.permissionMode case "plan": return r, errors.New("--permission-mode plan: plan mode is not supported — Codey has no read-only planning mode. Ask for a plan in the prompt, and use --disallowedTools Edit,Write,Bash to hold it to that") default: return r, fmt.Errorf("--permission-mode %q: want default, acceptEdits, bypassPermissions or dontAsk", c.permissionMode) } if r.YOLO { r.Mode = "bypassPermissions" } for i := range allow { allow[i].Root = root } for i := range deny { deny[i].Root = root } r.Rules = approval.RuleList{Allow: allow, Deny: deny} var whole []approval.Rule for _, d := range deny { if d.WholeTool() { whole = append(whole, d) } } r.Drop = func(name string) bool { for _, d := range whole { if d.Covers(name) { return true } } return false } r.DropHostedSearch = r.Drop("web_search") return r, nil } // isTerminal reports whether f is a terminal rather than a pipe or a file. It // asks the terminal driver rather than checking for a character device, // because /dev/null is one too — and `< /dev/null` is exactly how a CI job // says there is nobody here. func isTerminal(f *os.File) bool { return term.IsTerminal(int(f.Fd())) }