Files
mesh-catalog/modules/claude-code/cmd/claude-code/guard.go
T
jochen b57e6e8144 claude-code: write the agent's home as its own account where it has one
On the control node agents are to run as an account that cannot become root
(hq ADR 0266). The runtime keeps running as the operator, so the token, the
identity and the home-scope items go to the agent's home through a helper
started as that account; the licence and the account file follow the move,
what was placed in the operator's home goes once the agent holds the licence,
and the guard refuses every tool of a session running as anyone else,
recording to the journal. An API key is taken only from the agent's home and
only when it is one, here and in the licence manager, so the tool cannot carry
off the runtime's credential. Unchanged where no agent account is named.
2026-10-09 10:14:18 +02:00

812 lines
24 KiB
Go

package main
// The guard on the agent's shell (novox/hq ADR 0245): a PreToolUse hook every session on a machine of the
// mesh runs before a shell command or a file edit, delivered in the module's plugin.
//
// It refuses three work-arounds, each with the mesh tool that does the job when one says it replaces the
// command, and otherwise with the rule that a missing tool is created, never worked around:
//
// 1. **ssh to a mesh machine** — `ssh`, `scp`, `sftp`, `rsync`, `mosh` or `autossh` to a machine's name, a
// name under its domains, any `*.internal` name, or one of its addresses; a jump through one too. The
// destination is read as ssh itself reads it (`ssh -G`), so an alias in ~/.ssh/config is no way round.
// **Stated, not silent**: an ssh login as `git` is the forge's account, which runs git and nothing
// else — so `git push` over ssh, and `ssh -T git@<forge>`, reach no machine and pass. A git remote is
// never an ssh command line anyway.
// 2. **writing /etc/hosts or /etc/resolv.conf** — by redirect, tee, sed -i, an editor, cp/mv/install onto
// it, or the agent's own Edit and Write tools: a mesh name is the mesh's resolvers' (ADR 0194), and a
// machine's hosts file is its node-hostname seat's.
// 3. **HOSTALIASES**, named anywhere in a command line.
//
// And it refuses any command that names the operator's override, so the agent never sets it (guard_run.go).
//
// The matching is on the command line as a shell would split it — quotes, separators, `$(…)`, `bash -c` —
// and wrappers such as sudo, env and timeout are looked through. It is a guard against the habit of reaching
// for ssh, not a sandbox: a command built to hide what it runs can hide it, and the record of what was
// refused (the module's `guard.log`, read by `claude_code_guard`) is how a habit that found a way round is seen.
import (
"fmt"
"net"
"net/url"
"path"
"path/filepath"
"sort"
"strings"
)
// GuardData is what the hook is given: this machine, the mesh's machines, what replaces what, and where
// its record is kept. Written beside the hook on every render, root's, so the session cannot change it.
type GuardData struct {
Node string `json:"node"`
Machines []Machine `json:"machines"`
Replacements []Replacement `json:"replacements"`
Log string `json:"log"`
// Agent is the account every session here must run as, where the machine names an agent account of its
// own (novox/hq ADR 0266); empty where agents run as the operator.
Agent string `json:"agent,omitempty"`
}
// HookInput is what the agent hands a PreToolUse hook.
type HookInput struct {
ToolName string `json:"tool_name"`
ToolInput map[string]any `json:"tool_input"`
Cwd string `json:"cwd"`
SessionID string `json:"session_id"`
}
// Verdict is the guard's answer.
type Verdict struct {
Refuse bool `json:"refuse"`
Rule string `json:"rule,omitempty"` // ssh | hosts-file | hostaliases | override-named | account
Machine string `json:"machine,omitempty"`
Message string `json:"message,omitempty"`
// Overridable is false for a refusal no override lifts: the agent naming the override itself.
Overridable bool `json:"-"`
// Calls are the tools the refusal named, by address without a machine: what the instructions' table
// puts first, the tools the agent most reached round.
Calls []string `json:"calls,omitempty"`
}
// OverrideVar is the operator's override: set in the operator's own shell before the session starts,
// with why as its value. Read from the session's own environment as it was started, never from what a
// command or a setting put there (guard_run.go).
const OverrideVar = "MESH_GUARD_OVERRIDE"
// ForgeUser is the forge's ssh account: it runs git, never a shell (stated in ADR 0245).
const ForgeUser = "git"
// protectedFiles are the files a session never writes for a mesh name.
var protectedFiles = map[string]bool{"/etc/hosts": true, "/etc/resolv.conf": true}
// SSHView is how ssh itself reads a destination with these options: the host it connects to, the user,
// and the jumps. Empty host when ssh could not say.
type SSHView func(options []string, destination string) (host, user string, jumps []string)
// Guard judges what a session is about to do.
type Guard struct {
Data GuardData
// SSH reads a destination as ssh does (`ssh -G`); nil reads it from the command line alone.
SSH SSHView
// Resolve is a name's addresses, for a name that is none of the mesh's but may point at a machine.
Resolve func(string) []string
}
// JudgeAs is the verdict on one tool call of a session running as an account. **On a machine whose agents run
// as an account of their own, a session running as any other does nothing** (novox/hq ADR 0266): the
// operator's account there may become root without a person, and an agent started in it by habit would carry
// that into every command. Refused whatever the command, and no override lifts it — the operator's own shell is
// how the operator acts as themselves.
func (g Guard) JudgeAs(account string, in HookInput) Verdict {
if g.Data.Agent != "" && account != g.Data.Agent {
return Verdict{Refuse: true, Rule: "account", Message: fmt.Sprintf("Refused: on %s agents run as %s, an "+
"account that cannot become root without a person, and this session runs as %s (novox/hq ADR 0266). "+
"No tool it calls here is let through. End it, and start the agent with `claude-agent`, which runs it as "+
"%s; what needs the operator's own rights is the operator's to run in their own shell.",
orThisMachine(g.Data.Node), g.Data.Agent, account, g.Data.Agent)}
}
return g.Judge(in)
}
func orThisMachine(node string) string {
if node == "" {
return "this machine"
}
return node
}
// Judge is the verdict on one tool call.
func (g Guard) Judge(in HookInput) Verdict {
switch in.ToolName {
case "Bash":
command, _ := in.ToolInput["command"].(string)
return g.judgeLine(command, 0)
case "Edit", "Write", "MultiEdit", "NotebookEdit":
for _, key := range []string{"file_path", "notebook_path"} {
if p, _ := in.ToolInput[key].(string); p != "" && protectedFiles[filepath.Clean(p)] {
return g.hostsFile(filepath.Clean(p), in.ToolName+" "+filepath.Clean(p))
}
}
}
return Verdict{}
}
func (g Guard) judgeLine(line string, depth int) Verdict {
if depth > 4 {
return Verdict{}
}
if strings.Contains(line, OverrideVar) {
return Verdict{Refuse: true, Rule: "override-named", Message: fmt.Sprintf(
"Refused by the mesh's guard (novox/hq ADR 0245): this command names %s, the operator's override. "+
"It is the operator's alone, set in their own shell before a session starts; a session never sets, "+
"reads or passes it.", OverrideVar)}
}
if strings.Contains(line, "HOSTALIASES") {
return g.refuseLocal("hostaliases", "HOSTALIASES", "setting HOSTALIASES")
}
for _, words := range simpleCommands(line) {
if v := g.judgeCommand(words, depth); v.Refuse {
return v
}
// What a word runs in its own shell: `$(…)` and backticks inside a quoted word.
for _, w := range words {
for _, inner := range substitutions(w) {
if v := g.judgeLine(inner, depth+1); v.Refuse {
return v
}
}
}
}
return Verdict{}
}
// wrappers run the command after them; each with the options that take a value.
var wrappers = map[string]string{
"sudo": "ugCDhpTrt", "doas": "uC", "env": "uSC", "timeout": "sk", "nohup": "", "nice": "n", "ionice": "cnpP",
"command": "", "exec": "a", "time": "fo", "stdbuf": "ioe", "xargs": "aEdIiLlnPs", "setsid": "", "unbuffer": "",
"chronic": "", "flock": "wE", "torsocks": "", "proxychains": "f", "proxychains4": "f", "caffeinate": "",
}
// shells run their -c argument as a command line.
var shells = map[string]bool{"sh": true, "bash": true, "zsh": true, "dash": true, "ksh": true, "fish": true, "eval": true}
func (g Guard) judgeCommand(words []string, depth int) Verdict {
words = unwrap(words)
if len(words) == 0 {
return Verdict{}
}
name := path.Base(words[0])
args := words[1:]
if shells[name] {
if name == "eval" {
return g.judgeLine(strings.Join(args, " "), depth+1)
}
for i, a := range args {
if strings.HasPrefix(a, "-") && !strings.HasPrefix(a, "--") && strings.Contains(a, "c") && i+1 < len(args) {
return g.judgeLine(args[i+1], depth+1)
}
}
}
switch name {
case "ssh", "autossh", "mosh", "slogin":
return g.judgeSSH(name, args, strings.Join(words, " "))
case "scp", "sftp", "rsync":
return g.judgeCopy(name, args, strings.Join(words, " "))
}
if target := writesProtected(name, args); target != "" {
return g.hostsFile(target, strings.Join(words, " "))
}
return Verdict{}
}
// unwrap takes off what only runs the command: assignments before it, and wrappers with their options.
func unwrap(words []string) []string {
for len(words) > 0 {
w := words[0]
switch {
case w == "!" || w == "{" || w == "}" || w == "then" || w == "do" || w == "else" || w == "if" || w == "while" ||
w == "until" || w == "elif":
words = words[1:]
case isAssignment(w):
words = words[1:]
default:
opts, wrapper := wrappers[path.Base(w)]
if !wrapper {
return words
}
base := path.Base(w)
words = words[1:]
for len(words) > 0 {
a := words[0]
if a == "--" {
words = words[1:]
break
}
if base == "env" && isAssignment(a) {
words = words[1:]
continue
}
if !strings.HasPrefix(a, "-") || a == "-" {
break
}
words = words[1:]
if strings.HasPrefix(a, "--") {
continue
}
if f := a[len(a)-1:]; strings.Contains(opts, f) && len(words) > 0 {
words = words[1:]
}
}
// timeout's duration, nice's adjustment as a word: a number before the command.
if (base == "timeout") && len(words) > 0 {
words = words[1:]
}
}
}
return words
}
func isAssignment(w string) bool {
i := strings.Index(w, "=")
if i <= 0 {
return false
}
for _, r := range w[:i] {
if !(r == '_' || r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9') {
return false
}
}
return true
}
// sshOptionsWithValue are ssh's single-letter options that take a value.
const sshOptionsWithValue = "BbcDEeFIiJLlmOoPpQRSWw"
func (g Guard) judgeSSH(name string, args []string, line string) Verdict {
var options, jumps []string
user := ""
dest, rest := "", []string(nil)
for i := 0; i < len(args); i++ {
a := args[i]
if a == "--" {
if i+1 < len(args) {
dest, rest = args[i+1], args[i+2:]
}
break
}
if name == "autossh" && strings.HasPrefix(a, "-M") {
if a == "-M" {
i++ // autossh's own: the monitoring port
}
continue
}
if strings.HasPrefix(a, "-") && len(a) > 1 {
options = append(options, a)
if name == "mosh" || strings.HasPrefix(a, "--") {
continue // mosh's own options are long, each with its value after `=`
}
for j := 1; j < len(a); j++ {
if !strings.ContainsRune(sshOptionsWithValue, rune(a[j])) {
continue
}
value := a[j+1:]
if value == "" && i+1 < len(args) {
i++
value = args[i]
options = append(options, value)
}
switch a[j] {
case 'J':
jumps = append(jumps, strings.Split(value, ",")...)
case 'l':
user = value
case 'o':
k, v, _ := strings.Cut(value, "=")
switch strings.ToLower(strings.TrimSpace(k)) {
case "proxyjump":
jumps = append(jumps, strings.Split(v, ",")...)
case "user":
user = strings.TrimSpace(v)
}
}
break
}
continue
}
dest, rest = a, args[i+1:]
break
}
if dest == "" {
return Verdict{}
}
host, destUser := hostOf(dest)
if destUser != "" {
user = destUser
}
if g.SSH != nil && name != "mosh" {
if h, u, j := g.SSH(options, dest); h != "" {
host, user = h, u
jumps = append(jumps, j...)
}
}
remote := strings.Join(rest, " ")
for _, jump := range jumps {
jh, ju := hostOf(jump)
if machine, ok := g.meshHost(jh); ok && ju != ForgeUser {
return g.refuseSSH(machine, jh, remote, line)
}
}
if user == ForgeUser {
return Verdict{} // the forge's account: git and nothing else (stated in ADR 0245)
}
if machine, ok := g.meshHost(host); ok {
return g.refuseSSH(machine, host, remote, line)
}
return Verdict{}
}
// judgeCopy reads scp's, sftp's and rsync's remote paths, `[user@]host:path` or `scp://[user@]host/…`.
func (g Guard) judgeCopy(name string, args []string, line string) Verdict {
for i := 0; i < len(args); i++ {
a := args[i]
if strings.HasPrefix(a, "-") {
if name != "rsync" && len(a) == 2 && strings.ContainsRune("cFiJloPSXBRD", rune(a[1])) {
i++
}
if name == "rsync" && (a == "-e" || a == "--rsh") {
i++
}
continue
}
var host, user string
if strings.Contains(a, "://") {
host, user = hostOf(a)
} else if colon := strings.Index(a, ":"); colon > 0 && !strings.Contains(a[:colon], "/") {
host, user = hostOf(a[:colon])
} else if name == "sftp" {
host, user = hostOf(a)
} else {
continue
}
if user == ForgeUser {
continue
}
if g.SSH != nil {
if h, u, _ := g.SSH(nil, host); h != "" {
host = h
if u == ForgeUser && user == "" {
continue
}
}
}
if machine, ok := g.meshHost(host); ok {
return g.refuseSSH(machine, host, "", line)
}
}
return Verdict{}
}
// hostOf reads `[user@]host[:port]`, `ssh://[user@]host[:port]/…` or `[v6]`.
func hostOf(dest string) (host, user string) {
if strings.Contains(dest, "://") {
if u, err := url.Parse(dest); err == nil {
return strings.ToLower(u.Hostname()), u.User.Username()
}
}
if at := strings.LastIndex(dest, "@"); at >= 0 {
user, dest = dest[:at], dest[at+1:]
}
if strings.HasPrefix(dest, "[") {
if end := strings.Index(dest, "]"); end > 0 {
return strings.ToLower(dest[1:end]), user
}
}
if h, _, err := net.SplitHostPort(dest); err == nil && strings.Count(dest, ":") == 1 {
dest = h
}
return strings.TrimSuffix(strings.ToLower(dest), "."), user
}
// meshHost says whether a host is one of the mesh's machines, and which when it can tell.
func (g Guard) meshHost(host string) (string, bool) {
host = strings.TrimSuffix(strings.ToLower(strings.Trim(host, "[]")), ".")
if host == "" {
return "", false
}
if ip := net.ParseIP(host); ip != nil {
for _, m := range g.Data.Machines {
for _, a := range m.Addresses {
if b := net.ParseIP(a); b != nil && b.Equal(ip) {
return m.Name, true
}
}
}
return "", false
}
for _, m := range g.Data.Machines {
if host == strings.ToLower(m.Name) {
return m.Name, true
}
for _, d := range m.Domains {
if host == d || strings.HasSuffix(host, "."+d) {
return m.Name, true
}
}
}
if strings.HasSuffix(host, InternalSuffix) {
return "", true
}
if g.Resolve != nil && !strings.Contains(host, "/") {
for _, a := range g.Resolve(host) {
if m, ok := g.meshHost(a); ok && net.ParseIP(a) != nil {
return m, true
}
}
}
return "", false
}
// writesProtected is the protected file a command writes, or "".
func writesProtected(name string, args []string) string {
protected := func(a string) string {
a = strings.Trim(a, `"'`)
if strings.HasPrefix(a, "of=") {
a = a[3:]
}
if protectedFiles[filepath.Clean(a)] {
return filepath.Clean(a)
}
return ""
}
// A redirect onto it, from any command.
for i, a := range args {
if (a == ">" || a == ">>") && i+1 < len(args) {
if p := protected(args[i+1]); p != "" {
return p
}
}
}
operands := func() []string {
var out []string
for i := 0; i < len(args); i++ {
if args[i] == ">" || args[i] == ">>" || args[i] == "<" {
i++
continue
}
out = append(out, args[i])
}
return out
}()
switch name {
case "tee", "vi", "vim", "nvim", "nano", "emacs", "ed", "ex", "micro", "hx", "helix", "kak", "gedit", "code",
"truncate", "chattr", "chmod", "chown", "rm", "unlink", "shred", "dd", "sponge", "visudo", "vipw":
for _, a := range operands {
if p := protected(a); p != "" {
return p
}
}
case "sed", "perl", "ruby", "awk", "gawk":
inPlace := false
for _, a := range operands {
switch {
case name == "awk" || name == "gawk":
inPlace = inPlace || a == "inplace"
case strings.HasPrefix(a, "--in-place"):
inPlace = true
case strings.HasPrefix(a, "-") && !strings.HasPrefix(a, "--") && strings.Contains(a, "i"):
inPlace = true
}
}
if inPlace {
for _, a := range operands {
if p := protected(a); p != "" {
return p
}
}
}
case "cp", "mv", "install", "ln", "rsync":
var files []string
for _, a := range operands {
if !strings.HasPrefix(a, "-") {
files = append(files, a)
}
}
if len(files) > 1 {
if p := protected(files[len(files)-1]); p != "" {
return p
}
}
}
return ""
}
// replacementsFor are the mesh's tools that replace a command, the most specific first, each address once.
func (g Guard) replacementsFor(command string) []string {
have := commandWordsOf(command)
type found struct {
address string
weight int
replace string
}
var all []found
for _, r := range g.Data.Replacements {
best := 0
which := ""
for _, c := range r.Replaces {
want := commandWordsOf(c)
if inOrderWords(want, have) && len(want) > best {
best, which = len(want), c
}
}
if best > 0 {
all = append(all, found{r.Address, best, which})
}
}
sort.SliceStable(all, func(i, j int) bool { return all[i].weight > all[j].weight })
var out []string
for _, f := range all {
out = append(out, f.address+"\x00"+f.replace)
}
return out
}
// callFor is how an address is called for a machine.
func (g Guard) callFor(address, machine string) string {
for _, r := range g.Data.Replacements {
if r.Address != address || !r.NodeScoped {
continue
}
on := machine
if !r.Seat && len(r.On) > 0 && !containsString(r.On, machine) {
on = r.On[0]
}
if on == "" {
on = "<node>"
}
return on + "/" + address
}
return address
}
func containsString(xs []string, s string) bool {
for _, x := range xs {
if x == s {
return true
}
}
return false
}
// exact is the replacement that names this exact command, or "".
func (g Guard) exact(command string) string {
for _, r := range g.Data.Replacements {
for _, c := range r.Replaces {
if c == command {
return r.Address
}
}
}
return ""
}
const missingTool = "No mesh tool says it replaces this. Do not work around it: a missing tool is created in the module " +
"that owns the thing, on the seat it occupies — say what is missing to the operator. First make sure with " +
"`mesh_search` and the command's own words."
func (g Guard) refuseSSH(machine, host, remote, line string) Verdict {
who := machine
if who == "" {
who = host + " (a mesh name)"
}
var b strings.Builder
var named []string
fmt.Fprintf(&b, "Refused by the mesh's guard (novox/hq ADR 0245): `%s` reaches %s round the mesh's tools.\n", cut(line, 160), who)
switch {
case strings.TrimSpace(remote) == "":
target := machine
if target == "" {
target = "<machine>"
}
fmt.Fprintf(&b, "What a machine serves is `mesh_machine %s`; `mesh_search` with what you meant to run finds its tool.\n", target)
default:
calls := g.replacementsFor(remote)
if len(calls) == 0 {
b.WriteString(missingTool + "\n")
break
}
b.WriteString("Call instead, through `mesh_call`:\n")
for i, c := range calls {
if i == 3 {
break
}
address, replaces, _ := strings.Cut(c, "\x00")
named = append(named, address)
fmt.Fprintf(&b, " %s — replaces `%s`\n", g.callFor(address, machine), replaces)
}
}
return Verdict{Refuse: true, Rule: "ssh", Machine: machine, Message: strings.TrimRight(b.String(), "\n"), Overridable: true,
Calls: named}
}
func (g Guard) refuseLocal(rule, replaced, what string) Verdict {
var b strings.Builder
fmt.Fprintf(&b, "Refused by the mesh's guard (novox/hq ADR 0245): %s is a work-around for a mesh name. "+
"A mesh name is the mesh's resolvers' to answer, and a machine's own lines in /etc/hosts are its node-hostname seat's.\n", what)
var named []string
if address := g.exact(replaced); address != "" {
named = append(named, address)
fmt.Fprintf(&b, "Call instead, through `mesh_call`: %s — replaces `%s`", g.callFor(address, g.Data.Node), replaced)
} else {
b.WriteString(missingTool)
}
return Verdict{Refuse: true, Rule: rule, Machine: g.Data.Node, Message: b.String(), Overridable: true, Calls: named}
}
func (g Guard) hostsFile(file, line string) Verdict {
replaced := "edit " + file
return g.refuseLocal("hosts-file", replaced, fmt.Sprintf("writing %s (`%s`)", file, cut(line, 120)))
}
func cut(s string, n int) string {
s = strings.Join(strings.Fields(s), " ")
if r := []rune(s); len(r) > n {
return string(r[:n-1]) + "…"
}
return s
}
// commandWordsOf are a command line's words as matched against a replaced command (the mesh MCP server's search rule):
// lower-cased, a program by path reduced to its name, sudo left out.
func commandWordsOf(line string) []string {
var out []string
for i, w := range strings.Fields(strings.ToLower(line)) {
if i == 0 || strings.HasPrefix(w, "/") {
w = path.Base(w)
}
if w == "sudo" {
continue
}
out = append(out, w)
}
return out
}
func inOrderWords(want, have []string) bool {
i := 0
for _, h := range have {
if i < len(want) && h == want[i] {
i++
}
}
return len(want) > 0 && i == len(want)
}
// ---- the shell's words ------------------------------------------------------------------------------
// simpleCommands splits a command line as a shell would into its simple commands' words: quotes
// removed, `;`, `&`, `|`, `&&`, `||`, newlines, parentheses and backticks between commands, `$(` opening
// one; `>` and `>>` kept as words of their own, before what they redirect to.
func simpleCommands(line string) [][]string {
var out [][]string
var words []string
var cur strings.Builder
inWord := false
flushWord := func() {
if inWord {
words = append(words, cur.String())
cur.Reset()
inWord = false
}
}
flushCommand := func() {
flushWord()
if len(words) > 0 {
out = append(out, words)
}
words = nil
}
r := []rune(line)
for i := 0; i < len(r); i++ {
c := r[i]
switch {
case c == '\\' && i+1 < len(r):
i++
if r[i] != '\n' {
cur.WriteRune(r[i])
inWord = true
}
case c == '\'':
inWord = true
for i++; i < len(r) && r[i] != '\''; i++ {
cur.WriteRune(r[i])
}
case c == '"':
inWord = true
for i++; i < len(r) && r[i] != '"'; i++ {
if r[i] == '\\' && i+1 < len(r) && strings.ContainsRune("\"\\$`", r[i+1]) {
i++
}
cur.WriteRune(r[i])
}
case c == '$' && i+1 < len(r) && r[i+1] == '(':
flushCommand()
i++
case c == ';' || c == '&' || c == '|' || c == '\n' || c == '(' || c == ')' || c == '`':
flushCommand()
case c == '>' || c == '<':
// A file descriptor before it (`2>`) is not a word.
if inWord && isDigits(cur.String()) {
cur.Reset()
inWord = false
}
flushWord()
op := string(c)
if c == '>' && i+1 < len(r) && r[i+1] == '>' {
op = ">>"
i++
}
if i+1 < len(r) && (r[i+1] == '&' || r[i+1] == '|') {
i++
}
words = append(words, op)
case c == ' ' || c == '\t':
flushWord()
case c == '#' && !inWord:
for i < len(r) && r[i] != '\n' {
i++
}
flushCommand()
default:
cur.WriteRune(c)
inWord = true
}
}
flushCommand()
return out
}
func isDigits(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}
// substitutions are the command lines a quoted word runs in a shell of its own: `$(…)` and `…`.
func substitutions(word string) []string {
var out []string
for {
i := strings.Index(word, "$(")
if i < 0 {
break
}
rest := word[i+2:]
depth, end := 1, len(rest)
for j, c := range rest {
if c == '(' {
depth++
} else if c == ')' {
depth--
if depth == 0 {
end = j
break
}
}
}
out = append(out, rest[:end])
if end >= len(rest) {
break
}
word = rest[end+1:]
}
parts := strings.Split(word, "`")
for i := 1; i < len(parts); i += 2 {
out = append(out, parts[i])
}
return out
}