claude-code: point the agent at the mesh's tools and guard its shell against ssh to the mesh (hq ADR 0245)

The agent kept running ssh <machine> journalctl while the journal verb existed. The managed
instructions now carry an "instead of" table generated from what each verb says it replaces, and
the plugin carries a PreToolUse guard that refuses ssh to a mesh machine and local work-arounds for a
mesh name, naming the tool or saying one must be created. The operator's override is read from the
session's start environment and recorded.
This commit is contained in:
jochen
2026-10-07 20:27:31 +02:00
parent ba6058abb9
commit 24a3fac2ba
13 changed files with 2033 additions and 14 deletions
+34 -2
View File
@@ -324,15 +324,41 @@ func ConfigOf(items map[string]Item) Config {
// ---- rendering --------------------------------------------------------------------------------------
// PluginFile is one file of the marketplace: its content and whether it is run.
// PluginFile is one file of the marketplace: its content and whether it is run — or, for a binary, the
// file it is copied from (the guard's, which is this module's own executable).
type PluginFile struct {
Content string
Executable bool
From string `json:",omitempty"`
}
// GuardDir is where the guard on the agent's shell lives in the plugin (novox/hq ADR 0244): its binary and
// what it is given. Outside hooks/, so no registered hook's name can be it.
const GuardDir = "guard"
// GuardFiles are the guard as the plugin carries it: the binary to copy and the data it judges with.
type GuardFiles struct {
Exe string
Data GuardData
}
// GuardMatcher is what the guard is asked about: the shell, and the agent's own file edits.
const GuardMatcher = "Bash|Edit|Write|MultiEdit|NotebookEdit"
// GuardCommand is the hook's command line: by its path under the managed directory, root's — never the
// copy the agent may keep of a plugin, which the session could change.
func GuardCommand() string {
dir := filepath.Join(ManagedDir, MarketplaceDir, "plugins", Plugin, GuardDir)
return `"` + filepath.Join(dir, "guard") + `" guard "` + filepath.Join(dir, "guard.json") + `"`
}
// Marketplace is the marketplace directory's whole content, by path inside it. A node item of a name laid
// over a mesh item of the same kind and name: the node's wins.
func Marketplace(c Config) map[string]PluginFile {
func Marketplace(c Config) map[string]PluginFile { return MarketplaceWith(c, nil) }
// MarketplaceWith is the marketplace with the guard on the agent's shell, when there is one to place: its
// hook first, before any registered one.
func MarketplaceWith(c Config, guard *GuardFiles) map[string]PluginFile {
root := "plugins/" + Plugin + "/"
out := map[string]PluginFile{
".claude-plugin/marketplace.json": {Content: jsonFile(map[string]any{
@@ -359,6 +385,12 @@ func Marketplace(c Config) map[string]PluginFile {
}
sort.Strings(keys)
hooks := map[string][]any{}
if guard != nil && guard.Exe != "" {
out[root+GuardDir+"/guard"] = PluginFile{From: guard.Exe, Executable: true}
out[root+GuardDir+"/guard.json"] = PluginFile{Content: jsonFile(guard.Data)}
hooks["PreToolUse"] = append(hooks["PreToolUse"], map[string]any{"matcher": GuardMatcher,
"hooks": []any{map[string]any{"type": "command", "command": GuardCommand(), "timeout": 15}}})
}
for _, k := range keys {
it := chosen[k]
switch it.Kind {