# fix(trail): reject host-like --repo forge in the bare path form

`4a109f1`·

toothbrush·3w ago·4 files·+22 added/-4 removed

The bare forge/owner/repo form accepted parts[0] as the forge without
checking it is a known short forge id, so `--repo github.com/acme/app`
(or gitlab.com/...) slipped through and produced a malformed
/api/v1/trails/<forge>/... path that failed with an opaque server error.

Validate the forge segment against gitremote.IsSupportedForge and fail with a
clear message pointing at a forge id ("gh") or a clone URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

## Sessions

fb56d44a31fdView transcript

[?\Investigate CLI Repo and Branch FlagsClaude Code·Opus 4.8[1m]·2 steps](/content/gh/entireio/cli/session/974cdcd7-3d3a-4788-9c17-61e697d89e5a#timeline-fb56d44a31fd/index.html)

## Changes

4

- cmd/entire/cli

- agentimport

- Magentimport.go+1/-1

- gitremote

- Mgitremote.go+9

- Mtrail_cmd.go+9/-3

- Mtrail_repo_flag_test.go+3

```
69 unmodified lines

70
71
72
73
73
74
75
76

69 unmodified lines

// Get returns the importer registered under name.
//
//nolint:ireturn // Importer is the intended polymorphic seam returned to callers.

func Get(name string) (Importer, bool) {
	for _, imp := range importers {
		if imp.Name() == name {
```

Mcmd/entire/cli/agentimport/agentimport.go+1/-1

```
55 unmodified lines

56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

55 unmodified lines

return m
	}()

// IsSupportedForge reports whether forge is a known short forge id (e.g. "gh")
// understood by the trails API. It rejects forge hostnames ("github.com") and
// any other unrecognized value, so callers parsing a bare forge/owner/repo
// triple can fail clearly instead of forwarding a malformed forge to the API.
func IsSupportedForge(forge string) bool {
	_, ok := forgeToHost[forge]
	return ok
}

// CanonicalHost returns the canonical public host of the upstream forge.
//
// For direct git URLs this is just Host. For entire:// remotes — whose Host is
```

Mcmd/entire/cli/gitremote/gitremote.go+9

```
1721 unmodified lines

1722
1723
1724
1725
1726
1725
1726
1727
1728
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737

1721 unmodified lines

// fall through to the URL parser, which understands hosts and schemes.
	if !strings.Contains(raw, "://") && !strings.Contains(raw, "@"){ 
		parts := strings.Split(strings.Trim(raw, "/"), "/")
		if len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" {
			return parts[0], parts[1], strings.TrimSuffix(parts[2], ".git"), nil
		}
		if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" {
			return "", "", "", fmt.Errorf("invalid --repo %q: expected forge/owner/repo (e.g. gh/acme/app) or a clone URL", raw)
		}
		return "", "", "", fmt.Errorf("invalid --repo %q: expected forge/owner/repo (e.g. gh/acme/app) or a clone URL", raw)
		// parts[0] must be a short forge id ("gh"), not a hostname. A host-like
		// value (github.com/acme/app) would otherwise be forwarded verbatim and
		// the server would reject the malformed path with an opaque error.
		if !gitremote.IsSupportedForge(parts[0]) {
			return "", "", "", fmt.Errorf("invalid --repo %q: %q is not a supported forge id (use a forge id like \"gh\", or pass a clone URL such as https://github.com/%s/%s)", raw, parts[0], parts[1], parts[2])
		}
		return parts[0], parts[1], strings.TrimSuffix(parts[2], ".git"), nil
	}
	info, perr := gitremote.ParseURL(raw)
	if perr != nil {
```

Mcmd/entire/cli/trail_cmd.go+9/-3

```
28 unmodified lines

29
30
31
32
33
34
35
36
37

28 unmodified lines

{name: "forge plus owner only", raw: "gh/acme", wantErr: true},
		{name: "four segments", raw: "gh/acme/app/extra", wantErr: true},
		{name: "unsupported forge host", raw: "git@gitlab.com:acme/app.git", wantErr: true},
		{name: "bare host instead of forge id", raw: "github.com/acme/app", wantErr: true},
		{name: "bare unsupported forge host", raw: "gitlab.com/acme/app", wantErr: true},
		{name: "unknown short forge id", raw: "zz/acme/app", wantErr: true},
	}

for _, tt := range tests {
