Programmatically create Python components

With AI getting better at coding I’ve found myself looking less for custom grasshopper plugins and more working with AI to write one-off custom components that solve specific problems in the workflow, and the biggest bottle neck now is manually setting input name and expected type on the canvas.

It would be really cool if in the next iteration of grasshopper these graphical elements can also be automated programmatically. It might be hard to infer from the code itself but maybe let us plugin for example an XML or some other well-defined format that can be parsed, so we can use AI to generate that part of the component as well and get to work faster?

Or we can go the Kangaroo route and have python component having just one input, we plug everything into it, and have the script editor register each input with its name that can be referenced inside the script?

Thank you!

this is already possible, have a look at the fileformat *.ghx which is the XML version of the *.gh file.

Here is a MWE that computes a = x + y with a Python component. The script is contained as YSA9IHggKyB5 on line 303 as Base64. Decoding it yields the original python code.

a_eq_x_plus_y.ghx (33.5 KB)

is this creating inside canvas or our-right generate a full definition? I understand we can do the later but I’m more interested in the former so I can collaborate and manually control the definition more

not entirely sure, what you mean. maybe rephrase.

theoretically you can create components with python scripts and given payloads with inputs and outputs with this to the complexity your LLM of choice can handle.

or do you want to hook up python code to git for versioning and execute it in the canvas? this is also possible, i can explain.

I mean I want to be working within a normal manual definition, and drop in a python script component, and allow LLM to generate scripts (with or without MCP) and have the script to programmatically setup the input output format instead of me doing it by hand after copying in the script. Is that how you did it in your example (I need to wait till after the holiday to check your example file)?

Alternatively I imagine you can ask LLM to generate the ghx file directly that includes a python script component, which is helpful but (I assume) it’s only one-off and not interactive, meaning harder to work side by side with an LLM as I create some parts of the canvas, and let LLM insert a component when I need it to.

Something like this?

https://www.food4rhino.com/en/app/raven-ai?lang=en

No, no LLM was involved in this.

I am not sure, if a *.ghx can be changed in the background while it is open.

I used your method and was able to get a component in the generated .ghx file with the correct type hint, thank you!

If would still be much better if I can manually create a Python script component on a regular canvas in the XML format so I can use LLM to automate both the input type hint, names as well as the scripts in one go without manual modification, instead of having to create new ghx files.
Not really asking for Raven, just asking for a way to use script to control the input/output name and type of a python component. Maybe a way to drop a segment of XML in the current open definition to generate a list of connected components?

I’m only asking for it because for work environment it’s going to be a while before we can really use MCP so just trying to make the manual vibe-coding process smoother, thank you!

i am not sure, what changed, but type hints used to be automatic on connecting a wire to the component. at some point this behavior has changed, but that is a question for the mcneel team.

i also just edited the *.ghx file in an editor and hit CTRL+S in the canvas. apparently it has the following behavior:

  • Keep both: creates an incremented version with the new payload

  • replace: resets the edits in the editor to the original state.


not having looked further in it, by “Keep Both” you can manipulate *.ghx defintions and also get versioning, but it does not look very nice to use.

@Jun_Wang, your question about a programmatic way to flesh out script components got me wondering if that approach could also streamline the process when using MCPs. I’ve been exploring Claude with Cordyceps and rhinomcp recently and together they can set type hints and other aspects of script components beyond just the source code. But the process can be slow and clumsy at times.

So I worked with Claude to create a component that can “forge” script components based on raw source code input that includes a header of information like parameter type hints, name, icon, and such. No MCP required. The plugin and a GH definition of examples is attached. Note that it is experimental and likely buggy.

In this incarnation, the header isn’t a standard format like XML, but it is pretty straightforward to read and for an LLM to generate as part of the script.

The Script Forge plugin was compiled on macOS, so I’m not 100% sure it will run on Windows. If anyone tries it, please let me how it runs for you.

Script Forge Documentation

Script Forge

Script Forge is a Grasshopper component that builds other script components from plain text: feed it C# or Python 3 source and press a button, and it creates the component on the canvas — params, type hints, tooltips, name, and icon included. All that metadata comes from an optional @component comment header at the top of the source; without one, the code is simply injected into a stock script component. Point it at the same component again (via the Target input or the header’s guid: field) and it updates in place — wires stay on matching params, and nothing is ever duplicated when the source has not changed. Feed Source a tree — one branch per script — and a single forge fans out to one component per branch, each branch tracked, updated, and reported independently.

/* @component
name:        Remap Number
description: Remaps a number from one interval into another.

@in  Value  | double   | item | The number to remap.
@in  Source | Interval | item | The interval the value lives in.
@in  Target | Interval | item | The interval to map into.
@out Mapped | double   | item | The remapped number.
@end
*/

Fully standalone — stock Grasshopper + Rhino 8, no plugins.

Canvas usage

Param Type Access Meaning
Source (in) string tree The source text, one branch per script — each branch a list of lines (Read File output) or one multiline string. A plain list or single string is simply the one-branch case.
Target (in) any tree Optional component(s) to update: Guids, guid strings, or component references, paired with Source branches (see Forging several scripts at once). Falls back to each header’s guid: field; if both are absent a new component is created next to the forge.
Run (in) bool item Works with a toggle or a momentary button. While true (or on each press), forges every branch whose source text or target changed since the last run.
ComponentId (out) Guid tree Instance guid of the created/updated component, one branch per Source branch.
Log (out) string tree Step-by-step report per Source branch: parse, sync, lost wires, compile result, stamping, warnings.

Typical rig: Panel (file path) → Read File → Source, Button (or Boolean Toggle) → Run, and optionally a Panel (guid) → Target. The source can just as well come from a multiline panel with the whole script pasted in — anything that produces text works.

Getting a component’s guid (to update it in place):

  • easiest: the forge’s own ComponentId output is the guid of whatever it just forged — wire it to a panel and copy from there;
  • put a guid: field in the header instead — then the source itself pins its component and no Target wire is ever needed;
  • or use any component-info tool (e.g. Metahopper’s Object Info) — the Target input also accepts component reference goo directly, not just guid text.

Semantics worth knowing

  • Wires survive updates by param name first, then by position. Params whose names are unchanged keep their IGH_Param objects, so their wires stay. A param that was renamed in place is recycled positionally — the Log reports renamed X -> Y (n wire(s) kept) — so index-style renames keep their wires too. Only params that are truly removed lose wires, and each loss is reported.
  • Params end up in exactly the header’s order, so script bodies (or downstream tooling) that address params by index stay valid — the header is the index order.
  • Idempotent, button-friendly. The forge records what it last applied (in the document’s value table, saved inside the .gh), so recomputes, reopening the file, recompiling the forge, and repeated button presses do not create duplicates. It re-forges only when the source text or target changed, or when the previously forged component was deleted. To force a rebuild: edit the source, or delete the forged component.
  • While a toggle holds Run true, every input change forges immediately. Toggle Run off before re-pointing the source/target panels, or the transient in-between states will forge stray components. (A button avoids this by construction.)
  • A hidden out pin is fine. The stdout param is recognized by its type, never by name or index, so components whose standard output param is hidden (right-click → Standard Output/Error Parameter) update cleanly and stay hidden.
  • The languages must match on update: pushing a Python source at a C# Script component (or vice versa) is refused with an error.

Forging several scripts at once

Source is a tree: each branch is one script. Two panels through Entwine, a grafted list of multiline strings, several Read File outputs merged into branches — any tree shape works, and the single-script rigs above are just the one-branch case. All branches forge in one pass; ComponentId and Log come back as trees with matching paths.

Pairing targets. Each Source branch finds its Target as follows:

  1. a Target branch with the identical path → its first item;
  2. otherwise, if Target is a single flat list, the item at the branch’s position (first branch ↔ first item, and so on);
  3. otherwise (no match, or a null/empty item): the header’s guid: field, or a new component.

A null or empty entry in a Target list therefore means “forge a new component for this one” — so one list can mix updates and creations. The same component may not be targeted by two branches; the second claim errors (that branch only) and is skipped.

Per-branch state. The applied-source key and result guid are tracked per (forge instance, branch path), so re-running with one branch edited re-forges only that branch. The create/update contract, stated plainly:

  • no target → a new component per new (branch, source);
  • same source and target as last run → no-op;
  • target set (input or header guid:) → update in place.

One consequence: state follows the branch path, so reordering the branches of unpinned sources re-forges them as new components. Pin scripts via a Target or a header guid: when identity must survive reshuffles. Removing a branch leaves its forged component on canvas untouched; the branch’s saved state is pruned from the document.

The @component header

The header is the first comment block of the source. It is optional — see Headerless sources below.

/* @component
name:        Curve Frames
nickname:    Frames
category:    Curve
subcategory: Division
icon:        icons/curve-frames.svg
description: Divides a curve into equal-parameter segments and returns a frame
  at each division point. Long values wrap onto indented follow-on lines.

@in  Path       | Curve  | item | The curve to divide.
@in  Count      | int    | item | Number of segments.
@out Planes     | Plane  | list | One frame per division point.
@out Parameters | double | list | The curve parameter at each division point.
@end
*/

In Python use a module docstring or hash comments — same grammar, the parser strips the comment prefix per line:

"""@component
name: Point Grid PY
...
@end
"""

# @component
# name: Curve Divide PY
# ...
# @end

Fields

One per line, key: value. Unrecognized keys are ignored (forward-compatible).

Key Required Meaning
name yes Component Name (shown in tooltips and menus).
description yes The hover tooltip. No double quotes — see Warnings.
nickname no Canvas nickname. Defaults to name.
category / subcategory no Ribbon tab / panel. Informational for inline components.
icon no Path to an SVG or PNG icon, relative to the folder of the saved .gh. See Icons.
language no csharp or python. Usually unnecessary — see Language detection.
guid no Pins the target component instance. With a pinned guid the forge always updates that component (or recreates it with that exact guid), without needing the Target input.
marker no A literal tag string preserved verbatim in the source (for components discovered by source-text scans). May repeat.

Continuation lines

Any non-blank line that is not itself a key: value, @in/@out, or @end line continues the preceding value, joined with a single space. Indent continuations for readability. A blank line ends the continuation. Caveat: a continuation line that looks like a field (Options: a, b) will be parsed as a new field — rephrase, or keep the colon away from the line start.

Param lines

@in  Name | typehint | access | description
@out Name | typehint | access | description

  • Four fields split on the first three | only — the description may freely contain |, :, commas, and non-ASCII.
  • Name must match the RunScript argument name (C#). PascalCase by convention.
  • access is item (single value), list (one branch), or tree (multi-branch). In the C# signature: T ⇒ item, List<T> ⇒ list, DataTree<T> ⇒ tree.
  • Output hints are decorative in GH (outputs are typed by what you assign), but recording them keeps the header a complete spec.

Type hints

The typehint field selects the param’s Converter (right-click → Type Hint on a live param). Matching is case-insensitive; the same vocabulary works for C# and Python components. The full set on Rhino 8:

Hint Notes
bool, int, double, string primitives (double selects the converter Python names float; both spellings accepted)
object (or none, or blank) No Type Hint — raw goo, no conversion
Guid, DateTime, Complex, Color .NET / GH types
Point3d (point), Vector3d (vector), Plane (plane) frames & vectors
Interval, UVInterval GH Domain and Domain²
Line, Circle, Arc, Polyline, Rectangle3d primitive curves
Curve (curve) any curve
Box, Transform, Point3dList
Mesh (mesh), Surface, Extrusion, SubD, Brep (brep) surfaces & solids
PointCloud, GeometryBase GeometryBase accepts any geometry
Hatch, TextDot, TextEntity, Leader annotations

An unknown hint falls back to No Type Hint and the forge reports a HINT WARNING in the Log — hint typos never fail silently.

Python outputs are never hinted. On Python components an output type hint is an active converter applied to the assigned object wholesale, which breaks list outputs (PyObject to Point3d conversion errors). The forge leaves Python outputs on No Type Hint — matching stock components — and the header’s @out hints serve as documentation. (C# outputs are write-only object sinks, so their hints are applied but harmless either way.) The source push (set_Text) has a side effect of clearing a Python component’s Marshal Inputs / Outputs / Guids toggles, which would leave a returned list wrapped as one opaque PyObject — so the forge captures the three toggles before every push and restores them right after: a fresh component keeps the stock defaults (all on, lists unwrap into proper GH lists), an update keeps whatever the user has chosen.

Icons

icon: accepts three forms:

  • an SVG path — rasterized to a 24×24 PNG automatically (via macOS sips) next to the SVG; the PNG is reused until the SVG is newer;

  • a PNG path — used directly (make it 24×24; GH draws stamped icons at native pixel size);

  • an embedded base64 PNG — base64:<payload> (or a full data:image/png;base64, URI), so the icon travels inside the source with no file on disk. Long payloads may wrap across indented continuation lines; the joining whitespace is ignored by the decoder:

    icon: base64:iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNS
      R0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAA...
    
    

Path resolution: an absolute path is used as-is. A relative path is resolved against the folder of the saved .gh document — because the forge receives plain text, it cannot know where the source file lives (or whether it ever was a file), so the document’s own folder is the only stable anchor. Keep icons next to the .gh (e.g. an icons/ subfolder), or use an absolute path or a base64 payload when that doesn’t suit. If the document has never been saved, relative icons are skipped with a warning saying so. Keep icons square and legible at 24 px.

The icon step is strictly best-effort on every platform: a missing file, an unsupported extension, a corrupt image, an undecodable payload, or SVG rasterization off macOS each produce a clear icon warning: … line in the Log and the forge carries on — params, source, and the rest of the metadata are never affected. On Windows, use a PNG path or a base64 icon (or ship a pre-rasterized <name>.png next to the SVG); SVG-only icons are skipped there with a warning saying exactly that.

Language detection

Priority order:

  1. header language: field (csharp / python);
  2. a #! python shebang in the first lines;
  3. the header comment style — /* @component ⇒ C#, """@component or # @component ⇒ Python;
  4. for headerless updates, the target component’s own type;
  5. headerless heuristics — Script_Instance or using X; ⇒ C#; a leading docstring or import ⇒ Python.

If none of these decide, the forge errors and asks for a language: field.

Headerless sources

A source with no @component header is injected as-is: the forge creates a stock script component (default x, y inputs and out, a outputs) or pushes into the existing target, touching no params, metadata, or icon — exactly like dropping a default component and pasting the code in. The stock default C# and Python scripts work unchanged.

Warnings

The forge validates at forge time and reports problems in the Log (they never block the forge):

  • DRIFT WARNING — a header @in/@out name is missing from the C# RunScript signature, or vice versa. Drift is otherwise invisible: the canvas hints silently win over the signature at solve time.
  • QUOTE WARNING — a description contains a double quote. Fine on canvas, but it breaks the Rhino ScriptEditor plugin builder if the component is ever published (descriptions are embedded as unescaped C# string literals).
  • HINT WARNING — a type hint name was not recognized; the param got No Type Hint.
  • lost wire: … — a renamed/removed param dropped its connections.

Requirements and limits

  • Rhino 8 — targets the modern C# Script / Python 3 Script components (RhinoCodePluginGH). Legacy GHPython and the old C# script component are not supported as targets.
  • SVG icon rasterization uses macOS sips. On Windows, point icon: at a 24×24 PNG, embed it as base64:<payload>, or ship a pre-rasterized <name>.png next to the SVG. Icon problems of any kind only warn and never block the forge. Everything else is cross-platform.
  • One forge handles any number of scripts via Source branches, each with its own state (keyed by instance guid + branch path). Extra forge components also coexist cleanly — and yes, the forge can forge more forges.
  • The Log shows the previous run’s report until a new forge completes — the forge finishes a couple of solutions after the press (schedule → mutate → compile → stamp), which is normally imperceptible.

ScriptForge.gha (189 KB)

Script Forge examples.gh (32.4 KB)

Just an update to the Script Forge:

ScriptForge.gha (182 KB)

Script Forge 0.0.4 examples.gh (31.8 KB)

  • The previous example included a couple auxiliary components to identify which script components to update with changes to the source (e.g. by matching the name). That functionality has all been folded into the single Script Forge component. It can also take file paths as input directly so a Read File component is not required (but could still be used if you want to watch for changes to files on disk).
  • Data structure clarification.
  • The single Script Forge component can now be found in Params > Util.
  • Bug squashing and other behavior improvements.
Script Forge Component Reference

A reference for the Script Forge component itself: its four inputs, four
outputs, and the tree/branch data model that ties them together. It pairs with
the one-line tooltips stamped on each param — read those first for the gist,
this doc for the detail. The @component header grammar (how a source script
declares its own name, params, icon, etc.) lives in its own doc, Script Forge
Header Reference
; here the header only matters where it changes what a param
does.

Script Forge takes C# or Python 3 source (as text or a file path) and
creates or updates stock Grasshopper script components from it. Everything
below is about driving that from the four params — not about writing the
source. Stock Rhino 8 Grasshopper, no plugins.


The data model in one picture

Everything is organized around one idea: one Source branch = one script.

Source (tree)            Target (tree, optional)        Outputs (trees)
─────────────            ───────────────────────        ───────────────
{0}  script A    ─┐      {0}  guid, guid, …    ─┐        {0}  id, id, …   (one per target)
{1}  script B    ─┼──▶   {1}  'name'           ─┼──▶     {1}  id
{2}  script C    ─┘      (unwired = create)     ┘        {2}  id
      │                         │                              │
   each branch            targets that branch's         ComponentId / Objects /
   is one script          source is forged into         Success / Log all share
                                                         the Source branch paths
  • Source fans out: N branches → N (or more) forged components in one pass.
  • Target pairs to Source — per branch — and each branch may name several
    targets, so one script updates many components at once.
  • All four outputs mirror Source’s branch paths, so ComponentId,
    Objects, Success, and Log line up branch-for-branch and can be wired on
    together with no graft/flatten/path juggling.

A single un-treed input (one panel, one guid) is just the one-branch case — you
never have to think about trees until you want the fan-out.


Inputs

Source — string, tree access

The scripts to forge, one branch per script. A branch’s content can be:

  • Source text — a multiline string, or a list of lines (e.g. straight
    from a Read File component). The branch’s items are joined with newlines
    into one script.
  • A file path — a single line ending in .cs or .py. The forge reads the
    file itself; no Read File needed.
  • A list of paths — several path lines (or one multiline panel, one path per
    line). This expands to one component per path, exactly as if the list were
    grafted.

Detection is by shape (a path is a lone .cs/.py line; real source is
multiline and never all-path-looking), so text branches and path branches can
mix freely in one tree — there is no mode switch. Relative paths resolve against
the folder of the saved .gh; an unsaved document errors on relative paths.
Files are read only while Run is true, so path sources cost nothing at rest
(unlike a native Read File, which re-reads every solve).

Editing a file re-forges on the next Run. The forge reads current content,
so change detection sees the edit — what it does depends on the slot: a
pinned slot (a wired Target, or a header guid:) updates its component in
place, while an untargeted create-new slot forges a new component per
distinct source and leaves the previous one on the canvas. Pin a slot when an
edit must update the same component. A missing file errors only its own
branch (the others forge normally); a branch that is almost all paths — one
odd line among good ones — warns about a likely typo’d file name before treating
the whole branch as source text.

Access is tree deliberately. The component loops the branches itself in a
single solve rather than letting GH iterate, so every output path mirrors the
input path exactly ({i} in → {i} out). Feeding it a flat list still works — it’s
just a single branch {0}.

Target — object, tree access (optional)

Which component(s) each Source branch should update — or, left unwired, an
instruction to create new. Whether Target is wired at all is itself
meaningful, so the three states are distinct:

Target state Meaning
Unwired Create-new: each branch falls back to its header’s guid: field, else forges a brand-new component next to the forge.
Wired, with items for a branch Update those targets with that branch’s source.
Wired, but zero items for a branch Skip that branch — nothing forged, saved state untouched.

Each Target item may be:

  • A component reference — an instance Guid, a guid string, or component goo
    (e.g. from Get Objects by Name or Metahopper). Updates that exact component.
  • Null or an empty/whitespace item — the create-new marker; forges a new
    component in that slot. (One list can mix updates and creates.)
  • A keyword — name, nickname, name+create, or nickname+create
    (case-insensitive, the whole item). The source is forged into every
    on-canvas stock script component of its own language whose Name (or NickName)
    matches the source header’s own name. No match → skip, unless +create,
    which forges one new component. This replaces wiring a guid-lookup rig for the
    common “update all instances of this script” case.

How a branch finds its targets (first rule that applies wins):

  1. A Target branch with the identical path → all of its items (the
    source is forged into each — this is the many-targets fan-out).
  2. Else, if Target is a single flat list:
    • several Source branches → the item at the branch’s position (branch 0 ↔
      item 0, …);
    • one Source branch → every item targets it;
    • a flat list of only keywords and/or null/empty items →
      broadcasts to every branch (each branch re-resolves the keyword against
      its own header; a null forges one new component per branch). This is what
      lets a single name item update every instance of N different scripts
      in one pass.
  3. Target wired but resolving to nothing for a branch (empty matching
    branch, flat list ran out, no matching branch, keyword matched nothing) →
    skip.
  4. Target unwired → header guid:, else new component.

“Empty” means zero data items, not a blank panel. A blank/whitespace panel
still emits one empty string, which reads as the create-new marker and
forges a new component. To force a skip, feed the branch zero items.

The same component may not be targeted twice (across branches or within one
branch); the second claim errors that slot only and is skipped. The forge also
refuses its own InstanceGuid — it can’t forge itself.

Run — bool, item access

Set true to forge. Works with a Boolean Toggle or a momentary Button.
While true (or on each button press) it forges every branch that is out of
date
— meaning the source text, the target, or the target component’s own
script text
changed since the last run. An up-to-date branch is a no-op, so
holding Run true or mashing the button never duplicates anything.

Out-of-band edits are caught. After each forge the target’s settled script
text is hashed and stored; a later run whose target no longer matches that hash
re-forges the branch from Source (the Log says edited outside the forge). So
editing a forged component directly in the script editor is safe — the next Run
restores it from the source of truth.

Because state is idempotent, this is safe across recomputes, reopening the
.gh, and recompiling the forge. Caveat with a toggle: while it holds Run
true, every input change forges immediately, so toggle Run off before
re-pointing Source/Target panels or the in-between states forge strays. A button
sidesteps this by construction.

Force — bool, item access

While Run is true, re-forge every branch even when nothing changed —
bypasses the up-to-date skip for that one solve. Useful to restamp icons after
the .gh is first saved (relative icon paths resolve only once the doc has a
folder), or to rebuild wholesale without editing sources. Also a toggle or
button.


Outputs

All four are trees whose paths mirror the Source branches, and all four line
up slot-for-slot: within a branch there is one slot per target, so a branch
that fans out to 3 targets produces 3 aligned entries in each output.

ComponentId — Guid, tree

The instance guid of each forged component — one branch per Source branch, one
item per target. The value encodes the outcome:

Outcome ComponentId value
Created or updated OK the component’s guid
Failed update the target’s guid (so you still know which one failed)
Failed create an empty guid
Skipped branch empty (no items)

The forge’s own ComponentId is the easiest way to get a guid you forged —
wire it to a panel and copy it, or feed it back into a downstream tool.

Objects — object, tree

The forged components themselves, mirroring ComponentId slot-for-slot —
usable anywhere a component-reference input is expected (like the Objects
output of Get Objects by Name). A slot reads null when its component is not
on the canvas right now: deleted, never forged, or a create still in flight
(the settle solve fills it in a moment later). ComponentId still records the
guid in that case.

Success — bool, tree

One true/false per ComponentId slot: whether that slot’s last forge pass
finished without a forge error. Note the distinction — a target script’s own
runtime error does not flip Success false; only the forge’s own failures
(param sync, compile, stamping) do. Skipped branches stay empty. Wire
ComponentId + Success into Dispatch to split forged guids into succeeded
and failed lists.

Log — string, tree

A step-by-step report per Source branch — sectioned per target when a branch has
several — covering header parse, param sync, lost/renamed wires, compile result,
identity/icon stamping, and any warnings or errors. This is the diagnostic
surface; read it when a forge doesn’t do what you expected.

Timing. The Log (and Objects, and the in-flight slots) shows the
previous run’s report until the new forge completes. A forge finishes a
couple of solutions after the press (schedule → mutate → compile → stamp),
normally imperceptible. An in-flight slot reads Success true and settles when
the pass completes.

Reading the three id-outputs together — each slot is classified by combining
them: ComponentId non-empty + Success true = forged; ComponentId non-empty

  • Success false = failed update (that’s the target it failed on); empty
    ComponentId + Success false = failed create; all empty = skipped.

Updating in place

When a slot targets an existing component (a wired Target, a keyword match, or
a header guid:), the forge updates it rather than replacing it:

  • Wires survive by param name, then by position. Params whose names are
    unchanged keep their underlying param objects, so their wires stay put. A param
    renamed in place is recycled positionally — the Log reports
    renamed X -> Y (n wire(s) kept) — so index-style renames keep their wires
    too. Only a param that is genuinely removed loses its connections, and each
    loss is logged as lost wire: …. (Params end up in the header’s order — see
    the Header Reference.)
  • A hidden out pin is fine. The standard-output param is recognized by its
    type, never by name or index, so a component whose out is hidden
    (right-click → Standard Output/Error Parameter) updates cleanly and stays
    hidden.
  • Languages must match. The forge updates in place, and a C# component and a
    Python component are different classes, so a source only refreshes a target of
    its own language. Pushing Python source at a C# component (or vice versa)
    is refused with an error; to switch languages, delete the component and forge a
    new one (its wires are not carried over).

State, identity, and reshuffles

The forge remembers what it last applied per (forge instance, branch path, target slot), saved in the document’s value table inside the .gh. That’s why
presses, reopens, and recompiles don’t duplicate. Two consequences worth
internalizing:

  • State follows the branch path. Reordering the branches of unpinned
    (create-new) sources re-forges them as new components, because the path→state
    mapping shifted. Pin a script — via a Target wire or a header guid: —
    when its identity must survive branch reshuffles.
  • Removing a branch leaves its forged component on the canvas untouched; only
    the branch’s saved state is pruned from the document.

A wired-but-empty branch skipping (rather than creating) is what makes
match-by-name and Get Objects by Name rigs safe: sources whose component isn’t
on the canvas are quietly skipped instead of spawning strays.


Requirements & limits

  • Rhino 8. Targets are the modern C# Script / Python 3 Script components
    (RhinoCodePluginGH). The forge only updates stock components that carry
    editable source — pointing it at a compiled plugin component (or a legacy
    GHPython / old C# script component) errors, because there is no script slot to
    push into.
  • SVG icon rasterization uses macOS sips; on Windows use a PNG or base64
    icon (see the Header Reference). Icon problems only warn — they never block a
    forge.
  • Everything else is cross-platform, stock Grasshopper — no plugins.

Typical rigs

  • One script, create then keep updating: Panel (source or file path) →
    Source, Button → Run. First press creates; put the returned
    ComponentId in a panel, or add a header guid:, to keep updating in place.
  • Update every instance of a script: Source ← the script, Target ← a
    panel containing name. No lookup rig.
  • Forge many scripts at once: Entwine / grafted list / merged Read File
    outputs → Source (one branch each). Optionally a single name in Target
    broadcasts “update all instances” across all of them.
  • Update specific existing components: wire Get Objects by Name (or
    Metahopper) Objects → Target, matched branch-for-branch to Source.

For anything about the source content — the @component header, param type
hints, icons, language detection, headerless behavior — see Script Forge
Header Reference
.

Script Header Reference

How to write the @component header that makes a .cs or .py source
forgeable by Script Forge.cs. This is the metadata contract only; for
canvas wiring, targeting, and change-detection see Script Forge.md.

The header is optional. With no header the source is injected into a
stock script component as-is (params and metadata untouched). Add a header
when you want the forge to build params, type hints, tooltips, name, and
icon for you.


The shape

The header is the first comment block of the source. It opens with
@component, holds key: value fields and @in/@out param lines, and
closes with @end.

C# — a block comment:

/* @component
name:        Curve Frames
nickname:    Frames
category:    Curve
subcategory: Division
icon:        icons/curve-frames.svg
description: Divides a curve into equal-parameter segments and returns a
  frame at each division point.

@in  Path   | Curve  | item | The curve to divide.
@in  Count  | int    | item | Number of segments.
@out Planes | Plane  | list | One frame per division point.
@end
*/

Python — a module docstring, or hash comments. Same grammar; the parser
strips the comment prefix per line.

"""@component
name: Point Grid PY
description: Builds a grid of points.
@in  Count | int   | item | Points per side.
@out Pts   | Point | list | The grid points.
@end
"""
# @component
# name: Curve Divide PY
# description: Divides a curve.
# @end

Fields

One per line, key: value. Unrecognized keys are ignored — the header
is forward-compatible, so extra keys never break a forge.

Key Required Meaning
name yes Component Name (menus, tooltips). Also what the name target keyword matches.
description yes The hover tooltip. No double quotes (see Warnings).
nickname no Canvas nickname. Defaults to name. Matched by the nickname target keyword.
category no Ribbon tab. Informational for inline/on-canvas components.
subcategory no Ribbon panel. Informational for inline components.
icon no Path to an SVG/PNG, or an embedded PNG. See Icons.
language no csharp or python. Usually auto-detected (see Language detection).
guid no Pins the target instance: the forge always updates (or recreates) that exact component, no Target wire needed.
marker no A literal tag preserved verbatim in the source (for source-text scans). May repeat.

Project note: in this repo, published components also carry
exposure: level1 and subcategory: Experimental by convention. exposure
is not yet parsed by the shared tooling — see the project CLAUDE.md.


Param lines

@in  Name | typehint | access | description
@out Name | typehint | access | description
@inout Name | ...              (rare — a param that is both)
  • Split on the first three | only — the description may freely contain
    |, :, commas, and non-ASCII.

  • Name must match the RunScript argument name (C#), PascalCase by
    convention. A mismatch triggers a DRIFT WARNING.

  • Params end up in exactly the header’s order. Index-based body code and
    downstream tooling stay valid — the header is the index order.

  • access is one of:

    access C# signature Meaning
    item T a single value
    list List<T> one branch
    tree DataTree<T> multi-branch
  • Output hints are decorative in GH (outputs are typed by what you
    assign), but recording them keeps the header a complete spec.

Type hints

The typehint field selects the param’s Converter. Matching is
case-insensitive; the same vocabulary works for C# and Python.

Hint Notes
bool, int, double, string primitives (double = the converter Python names float; both spellings work)
object / none / blank No Type Hint — raw goo, no conversion
Guid, DateTime, Complex, Color .NET / GH types
Point3d (point), Vector3d (vector), Plane (plane) frames & vectors
Interval, UVInterval Domain and Domain²
Line, Circle, Arc, Polyline, Rectangle3d primitive curves
Curve (curve) any curve
Box, Transform, Point3dList
Mesh (mesh), Surface, Extrusion, SubD, Brep (brep) surfaces & solids
PointCloud, GeometryBase GeometryBase accepts any geometry
Hatch, TextDot, TextEntity, Leader annotations

An unknown hint falls back to No Type Hint and the forge logs a
HINT WARNING — hint typos never fail silently.

Python outputs are never hinted. On Python components an output hint is
an active converter that breaks list outputs. The forge leaves Python
outputs on No Type Hint (matching stock components); the header @out hints
serve as documentation there. C# outputs are write-only, so their hints are
applied but harmless.


Continuation lines

Any non-blank line that is not itself a key: value, @in/@out, or
@end line continues the previous value, joined with a single space.
Indent continuations for readability. A blank line ends the continuation.

description: Remaps a number from one interval into another, clamping
  values that fall outside the source interval.

Caveat: a continuation that looks like a field (Options: a, b) is
parsed as a new field. Rephrase, or keep the colon away from the line start.

Multi-line tooltips: the \n token

Continuations always join with a space, so to force a real line break in a
stamped tooltip use a literal \n token — in description: or any
@in/@out description:

@in Target | object | tree | Which component each branch updates:\n• a guid\n• the keyword name\n• null to create new

The token is converted at stamp time only. The header grammar is
unchanged, so every parser (this forge, Script Meta, gh_meta.py) passes it
through untouched, and publishing embeds it as a C# string literal that
compiles to the same newline.


Icons

icon: accepts three forms:

  • SVG path — rasterized to a 24×24 PNG automatically via macOS sips
    (reused until the SVG is newer);

  • PNG path — used directly (make it 24×24; GH draws at native pixel size);

  • embedded PNG — base64:<payload> or a full data:image/png;base64,
    URI, so the icon travels inside the source. Long payloads may wrap across
    indented continuation lines; the joining whitespace is ignored.

    icon: base64:iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNS
      R0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAA...
    

Path resolution: absolute paths are used as-is; relative paths resolve
against the folder of the saved .gh
(the forge only sees text — it can’t
know where the source file lives). Keep icons next to the .gh (e.g. an
icons/ subfolder). In an unsaved document, relative icons are skipped with a
warning.

Icons are strictly best-effort. A missing file, bad extension, corrupt
image, or SVG rasterization off macOS produces an icon warning: in the Log
and the forge carries on — params, source, and other metadata are unaffected.
On Windows, use a PNG path or a base64 icon (SVG-only icons are skipped
there).


Language detection

The forge picks the language in this order:

  1. header language: field (csharp / python);
  2. a #! python shebang in the first lines;
  3. the header comment style — /* @component ⇒ C#, """@component or
    # @component ⇒ Python;
  4. for headerless updates, the target component’s own type;
  5. headerless heuristics — Script_Instance or using X; ⇒ C#; a leading
    docstring or import ⇒ Python.

If none decide, the forge errors and asks for a language: field. Add
language: whenever the language would otherwise be ambiguous.


Headerless sources

A source with no header is injected as-is: the forge creates a stock
component (default x, y inputs / out, a outputs) or pushes into the target,
touching no params, metadata, or icon.

A headerless create leans entirely on the heuristics above, so a
marker-less body (e.g. a bare a = x + y) can’t be classified and errors —
add a recognizable marker (import, a docstring, a using) or give it a
header. A headerless update never hits this: the target’s own type settles
the language.


Warnings

Validated at forge time and reported in the Log — they never block the
forge
:

Warning Cause
DRIFT WARNING a header @in/@out name is missing from the RunScript signature, or vice versa.
QUOTE WARNING a description contains a " — fine on canvas, but breaks the ScriptEditor plugin builder if published.
HINT WARNING a type-hint name wasn’t recognized; the param got No Type Hint.
lost wire: … a renamed/removed param dropped its connections.

Complete example

/* @component
name:        Remap Number
nickname:    Remap
category:    Maths
subcategory: Domain
icon:        icons/remap-number.svg
description: Remaps a number from one interval into another. Values outside
  the source interval are extrapolated, not clamped.
guid:        3f2a9c10-8b41-4e6d-9a2f-1c7e5b0d4a88

@in  Value  | double   | item | The number to remap.
@in  Source | Interval | item | The interval the value currently lives in.
@in  Target | Interval | item | The interval to map into.
@out Mapped | double   | item | The remapped number.
@end
*/

using System;
using Grasshopper.Kernel;

public class Script_Instance : GH_ScriptInstance
{
  private void RunScript(double Value, Rhino.Geometry.Interval Source,
    Rhino.Geometry.Interval Target, out object Mapped)
  {
    double t = Source.NormalizedParameterAt(Value);
    Mapped = Target.ParameterAt(t);
  }
}

This is really cool Anthony, let me check it out, I was too dumb and didn’t ask Claude for it lol

Thank you Dan that’s very helpful!

This is really cool Anthony! The fact that it can work with a python file means I can achieve pseudo agentic process without a MCP which has IT implications.
Edit:
it really works now, you should consider releasing this as a real plugin in package manager!

Thnx again!

Thanks for trying it out. I’m glad you got it working. In your screenshot it looks like you just needed to change the Source Path set in the panel to the location of the actual files on your computer.

The latest version of Script Forge eliminates the need for a read file component. You can plug the path directly into the Source param. And if the .gh file is in the same directory as the scripts, you can simply supply the script names rather than the full path.

Script Forge now installs from a yak package rather than manual installation of a .gha.

scriptforge-0.2.0-beta-rh8_33-any.yak (39.5 KB)

I have been considering releasing it in the public package manager but am still getting my feet under me regarding plugin development and publishing. This has been a learning process for sure.

The biggest update in the new version is a switch to a JSON header format. This clarifies and standardizes the syntax. It also introduces the ability to set default values for params.

An updated set of examples including separate Python and C# script files along with icons is attached below.

Script Forge 0.2.0-beta examples.zip (112.9 KB)

Updated documentation:

Script Forge Component Reference

Script Forge Component Reference

A reference for the Script Forge component itself: its four inputs, four outputs, and the tree/branch data model that ties them together. It pairs with the one-line tooltips stamped on each param — read those first for the gist, this doc for the detail. The @component header format (how a source script declares its own name, params, icon, etc.) lives in its own doc, Script Forge Header Reference; here the header only matters where it changes what a param does.

Script Forge takes C# or Python 3 source (as text or a file path) and creates or updates stock Grasshopper script components from it. Everything below is about driving that from the four params — not about writing the source. Stock Rhino 8 Grasshopper, no plugins.


The data model in one picture

Everything is organized around one idea: one Source branch = one script.

Source (tree)            Target (tree, optional)        Outputs (trees)
─────────────            ───────────────────────        ───────────────
{0}  script A    ─┐      {0}  guid, guid, …    ─┐        {0}  id, id, …   (one per target)
{1}  script B    ─┼──▶   {1}  'name'           ─┼──▶     {1}  id
{2}  script C    ─┘      (unwired = create)     ┘        {2}  id
      │                         │                              │
   each branch            targets that branch's         ComponentId / Objects /
   is one script          source is forged into         Success / Log all share
                                                         the Source branch paths

  • Source fans out: N branches → N (or more) forged components in one pass.
  • Target pairs to Source — per branch — and each branch may name several targets, so one script updates many components at once.
  • All four outputs mirror Source’s branch paths, so ComponentId, Objects, Success, and Log line up branch-for-branch and can be wired on together with no graft/flatten/path juggling.

A single un-treed input (one panel, one guid) is just the one-branch case — you never have to think about trees until you want the fan-out.


Inputs

Source — string, tree access

The scripts to forge, one branch per script. A branch’s content can be:

  • Source text — a multiline string, or a list of lines (e.g. straight from a Read File component). The branch’s items are joined with newlines into one script.
  • A file path — a single line ending in .cs or .py. The forge reads the file itself; no Read File needed.
  • A list of paths — several path lines (or one multiline panel, one path per line). This expands to one component per path, exactly as if the list were grafted.

Detection is by shape (a path is a lone .cs/.py line; real source is multiline and never all-path-looking), so text branches and path branches can mix freely in one tree — there is no mode switch. Relative paths resolve against the folder of the saved .gh; an unsaved document errors on relative paths. Files are read only while Run is true, so path sources cost nothing at rest (unlike a native Read File, which re-reads every solve).

Editing a file re-forges it immediately. While Run is true the forge watches every file-path source on disk: save the file in your editor and it re-forges a moment later, with no second press and no Read File rig. Write, save, watch the component change. (The watch is on each containing folder, filtered down to the exact paths and debounced, so editors that save via a temp file and a rename are caught too; setting Run false tears every watcher down.)

What the re-forge does depends on the slot: a pinned slot (a wired Target, or a header instanceGuid) updates its component in place, while an untargeted create-new slot forges a new component per distinct source and leaves the previous one on the canvas. Pin a slot when an edit must update the same component. A missing file errors only its own branch (the others forge normally); a branch that is almost all paths — one odd line among good ones — warns about a likely typo’d file name before treating the whole branch as source text.

Access is tree deliberately. The component loops the branches itself in a single solve rather than letting GH iterate, so every output path mirrors the input path exactly ({i} in → {i} out). Feeding it a flat list still works — it’s just a single branch {0}.

Target — object, tree access (optional)

Which component(s) each Source branch should update — or, left unwired, an instruction to create new. Whether Target is wired at all is itself meaningful, so the three states are distinct:

Target state Meaning
Unwired Create-new: each branch falls back to its header’s instanceGuid, else forges a brand-new component next to the forge.
Wired, with items for a branch Update those targets with that branch’s source.
Wired, but zero items for a branch Skip that branch — nothing forged, saved state untouched.

Each Target item may be:

  • A component reference — an instance Guid, a guid string, or component goo (e.g. from Metahopper). Updates that exact component.
  • Null or an empty/whitespace item — the create-new marker; forges a new component in that slot. (One list can mix updates and creates.) If the source’s header carries an instanceGuid, that pin fills in here instead — see rule 4 below.
  • A keyword — name, nickname, name+create, or nickname+create (case-insensitive, the whole item). The source is forged into every on-canvas stock script component of its own language whose Name (or NickName) matches the source header’s own name. No match → skip, unless +create, which forges one new component. This replaces wiring a guid-lookup rig for the common “update all instances of this script” case.

How a branch finds its targets (first rule that applies wins):

  1. A Target branch with the identical path → all of its items (the source is forged into each — this is the many-targets fan-out).
  2. Else, if Target is a single flat list:
    • several Source branches → the item at the branch’s position (branch 0 ↔ item 0, …);
    • one Source branch → every item targets it;
    • a flat list of only keywords and/or null/empty items → broadcasts to every branch (each branch re-resolves the keyword against its own header; a null forges one new component per branch). This is what lets a single name item update every instance of N different scripts in one pass.
  3. Target wired but resolving to nothing for a branch (empty matching branch, flat list ran out, no matching branch, keyword matched nothing) → skip.
  4. The slot names no component — Target unwired, or the item was the create-new marker → the source header’s instanceGuid, else a new component. A header pin therefore beats “create new”: a pinned source re-forges into its own component rather than dropping a copy. Pinning is sticky on purpose.

“Empty” means zero data items, not a blank panel. A blank/whitespace panel still emits one empty string, which reads as the create-new marker and forges a new component. To force a skip, feed the branch zero items.

The same component may not be targeted twice (across branches or within one branch); the second claim errors that slot only and is skipped. The forge also refuses its own InstanceGuid — it can’t forge itself.

Run — bool, item access

Set true to forge. Works with a Boolean Toggle or a momentary Button. While true (or on each button press) it forges every branch that is out of date — meaning the source text, the target, or the target component’s own script text changed since the last run. An up-to-date branch is a no-op, so holding Run true or mashing the button never duplicates anything.

Out-of-band edits are caught. After each forge the target’s settled script text is hashed and stored; a later run whose target no longer matches that hash re-forges the branch from Source (the Log says edited outside the forge). So editing a forged component directly in the script editor is safe — the next Run restores it from the source of truth.

Because state is idempotent, this is safe across recomputes and across closing and reopening the .gh.

Toggle or button? They are genuinely different tools:

  • A toggle holds Run true, which is what arms the file watcher — leave it on and every save of a file-path source re-forges by itself. The cost is that while it is on, every input change forges immediately, so switch it off before re-pointing Source/Target panels or the in-between states forge strays.
  • A button forges once per press and sidesteps that by construction. Nothing is watched between presses.

Leave the toggle on for a write-save-look iteration loop; use the button when you are rewiring the rig itself.

Force — bool, item access

While Run is true, re-forge every branch even when nothing changed — bypasses the up-to-date skip for that one solve. Useful to restamp icons after the .gh is first saved (relative icon paths resolve only once the doc has a folder), or to rebuild wholesale without editing sources. Also a toggle or button.


Outputs

All four are trees whose paths mirror the Source branches, and all four line up slot-for-slot: within a branch there is one slot per target, so a branch that fans out to 3 targets produces 3 aligned entries in each output.

ComponentId — Guid, tree

The instance guid of each forged component — one branch per Source branch, one item per target. The value encodes the outcome:

Outcome ComponentId value
Created or updated OK the component’s guid
Failed update the target’s guid (so you still know which one failed)
Failed create an empty guid
Skipped branch empty (no items)

The forge’s own ComponentId is the easiest way to get a guid you forged — wire it to a panel and copy it, or feed it back into a downstream tool.

Objects — object, tree

The forged components themselves, mirroring ComponentId slot-for-slot — usable anywhere a component-reference input is expected. A slot reads null when its component is not on the canvas right now: deleted, never forged, or a create still in flight (the settle solve fills it in a moment later). ComponentId still records the guid in that case.

Success — bool, tree

One true/false per ComponentId slot: whether that slot’s last forge pass finished without a forge error. Note the distinction — a target script’s own runtime error does not flip Success false; only the forge’s own failures (param sync, compile, stamping) do. Skipped branches stay empty. Wire ComponentId + Success into Dispatch to split forged guids into succeeded and failed lists.

Log — string, tree

A step-by-step report per Source branch — sectioned per target when a branch has several — covering header parse, param sync, lost/renamed wires, compile result, identity/icon stamping, and any warnings or errors. This is the diagnostic surface; read it when a forge doesn’t do what you expected.

Timing. The Log (and Objects, and the in-flight slots) shows the previous run’s report until the new forge completes. A forge finishes a couple of solutions after the press (schedule → mutate → compile → stamp), normally imperceptible. An in-flight slot reads Success true and settles when the pass completes.

Reading the three id-outputs together — each slot is classified by combining them: ComponentId non-empty + Success true = forged; ComponentId non-empty

  • Success false = failed update (that’s the target it failed on); empty ComponentId + Success false = failed create; all empty = skipped.

Updating in place

When a slot targets an existing component (a wired Target, a keyword match, or a header instanceGuid), the forge updates it rather than replacing it:

  • Wires survive by param name, then by position. Params whose names are unchanged keep their underlying param objects, so their wires stay put. A param renamed in place is recycled positionally — the Log reports renamed X -> Y (n wire(s) kept) — so index-style renames keep their wires too. Only a param that is genuinely removed loses its connections, and each loss is logged as lost wire: …. (Params end up in the header’s order — see the Header Reference.)
  • A hidden out pin is fine. The standard-output param is recognized by its type, never by name or index, so a component whose out is hidden (right-click → Standard Output/Error Parameter) updates cleanly and stays hidden.
  • Languages must match. The forge updates in place, and a C# component and a Python component are different classes, so a source only refreshes a target of its own language. Pushing Python source at a C# component (or vice versa) is refused with an error; to switch languages, delete the component and forge a new one (its wires are not carried over).

State, identity, and reshuffles

The forge remembers what it last applied per (forge instance, branch path, target slot), saved in the document’s value table inside the .gh. That’s why repeated presses and document reopens don’t duplicate. Two consequences worth internalizing:

  • State follows the branch path. Reordering the branches of unpinned (create-new) sources re-forges them as new components, because the path→state mapping shifted. Pin a script — via a Target wire or a header instanceGuid — when its identity must survive branch reshuffles.
  • Removing a branch leaves its forged component on the canvas untouched; only the branch’s saved state is pruned from the document.

A wired-but-empty branch skipping (rather than creating) is what makes match-by-name and guid-lookup rigs safe: sources whose component isn’t on the canvas are quietly skipped instead of spawning strays.

A header’s declared default seeds an empty param only. Where a source declares a default value for an input, the forge writes it into that param’s persistent data — the internalized value Grasshopper hands the script when nothing is wired in. It does this only when the param has no persistent data of its own. Persistent data is yours to edit (right-click ▸ Set Boolean, Internalise data), and a forge re-runs on every source edit, so re-stamping it would quietly undo what you typed — several times a minute while iterating. The cost of that rule is the other direction: a header can seed a default but never reset one. Clearing is yours too — right-click ▸ Destroy persistent data, then re-forge to pick the header’s value back up.


Requirements & limits

  • Rhino 8. Targets are the modern C# Script / Python 3 Script components (RhinoCodePluginGH). The forge only updates stock components that carry editable source — pointing it at a compiled plugin component (or a legacy GHPython / old C# script component) errors, because there is no script slot to push into.
  • SVG icon rasterization uses macOS sips; on Windows use a PNG or base64 icon (see the Header Reference). Icon problems only warn — they never block a forge.
  • Everything else is cross-platform, stock Grasshopper — no plugins.

The component lives on the Params ▸ Util panel of the ribbon.


Typical rigs

  • One script, create then keep updating: Panel (source or file path) → Source, Button → Run. First press creates; put the returned ComponentId in a panel, or add a header instanceGuid, to keep updating in place.
  • Live editing loop: Panel holding a .cs/.py path → Source, Boolean Toggle set true → Run. Now edit the file in your own editor: each save re-forges the component on the canvas by itself. Give the source a header instanceGuid (or wire Target) so every save updates the same component instead of forging a new one.
  • Update every instance of a script: Source ← the script, Target ← a panel containing name. No lookup rig.
  • Forge many scripts at once: Entwine / grafted list / merged Read File outputs → Source (one branch each). Optionally a single name in Target broadcasts “update all instances” across all of them.
  • Update specific existing components: wire a component-reference source (e.g. Metahopper’s Objects) → Target, matched branch-for-branch to Source.

For anything about the source content — the @component header, param type hints, icons, language detection, headerless behavior — see Script Forge Header Reference.

Script Forge Header Reference

Script Forge Header Reference

How to write the @component header that makes a .cs or .py source forgeable by Script Forge — the Grasshopper component that creates or updates C# and Python 3 script components from source text. This documents the header contract only; canvas wiring, targeting, and change-detection live in the companion doc, Script Forge — Component Reference.

The header is optional. With no header the source is injected into a stock script component as-is (params and metadata untouched). Add a header when you want the forge to build params, type hints, tooltips, name, and icon for you.


The shape

The header is the first comment block of the source. It opens with @component and its body is one JSON object — the object’s own closing brace ends the header, so there is no terminator keyword.

C# — a block comment:

/* @component
{
  "name":        "Curve Frames",
  "nickname":    "Frames",
  "icon":        "icons/curve-frames.svg",
  "description": "Divides a curve into equal-parameter segments and returns a frame at each division point.",

  "inputs": [
    { "name": "Path", "type": "Curve", "access": "item",
      "description": "The curve to divide." },
    { "name": "Count", "type": "int", "access": "item", "default": 10,
      "description": "Number of segments." }
  ],

  "outputs": [
    { "name": "Planes", "type": "Plane", "access": "list",
      "description": "One frame per division point." }
  ]
}
*/

Python — a module docstring, or hash comments. Same body; the parser strips the comment prefix per line, which is safe because a JSON string can never span a line break.

"""@component
{
  "name": "Point Grid PY",
  "description": "Builds a grid of points.",
  "inputs":  [ { "name": "Count", "type": "int", "access": "item",
                 "description": "Points per side." } ],
  "outputs": [ { "name": "Pts", "type": "Point3d", "access": "list",
                 "description": "The grid points." } ]
}
"""

# @component
# {
#   "name": "Curve Divide PY",
#   "description": "Divides a curve."
# }

It is strict JSON. No comments, no trailing commas, no single quotes — a stock JSON parser refuses all three. A syntax error is reported with its line and column, and the forge stops on that branch and forges the others.

Anything after the closing brace is ignored, which is what lets the */ or """ sit there. Unrecognized keys are ignored — extra keys never break a forge (see Keys the forge ignores).


Component keys

Key Required Meaning
name yes Component Name (menus, tooltips). Also what the name target keyword matches.
description yes The hover tooltip. No double quotes (see Warnings).
nickname no Canvas nickname. Defaults to name. Matched by the nickname target keyword.
icon no Path to an SVG/PNG, or an embedded PNG. See Icons.
language no csharp or python. Usually auto-detected (see Language detection).
instanceGuid no Pins the source to one component: with no Target wire, the forge updates (or recreates) that exact instance.
inputs / outputs no Arrays of param objects. See below.

instanceGuid — pinning a source to one component

instanceGuid is the InstanceGuid of a component on the canvas: the one this source owns. It is what makes an edit-and-re-forge loop land on the same component every time instead of dropping a new one beside it. Forge the source once with no header guid, read the ComponentId the forge returns, and paste it into the header.

It is the fallback, not an override. A Target item naming an actual component wins — so a pinned source can still be pushed somewhere else deliberately. The header guid fills in whenever the slot names no component: Target unwired, or a Target item that is the create-new marker (null or empty). That second case surprises people. A pinned source asked to “create new” doesn’t give you a second copy; it re-pins to its own guid, and the Log says using header instanceGuid as target. Pinning is sticky on purpose — it is what stops an iteration loop from littering the canvas.

It recreates, too. If the pinned component isn’t on the canvas — you deleted it, or you opened a fresh document — the forge creates one and gives it that guid, so the pin keeps working. Delete a forged component, re-forge, and it comes back with the same identity (its wires do not).

There is no need to invent a guid by hand, and no reason to change one: it is the component’s identity, and rewriting it orphans whatever the old value pointed at.

guid is not a valid key. An older header grammar used a bare guid:, and it meant two different things depending on which tool read it. The forge rejects it by name rather than ignoring it — being ignored would silently stop the header pinning anything — with a message naming the replacement. Say instanceGuid.

Keys the forge ignores

category, subcategory, exposure, componentGuid, upgradeFrom and markers are read by build tooling that compiles a source into a permanent plugin component; a forged component’s ribbon placement and identity are not the header’s to set. The forge parses straight past them, so a source written for such a build forges unchanged — and so does one carrying any other extra key. Unrecognized keys are ignored by design. guid, above, is the one exception.


Param objects

Each entry of inputs / outputs is a JSON object:

{ "name": "Keys", "variableName": "OutKeys", "type": "string", "access": "list",
  "optional": false, "default": null,
  "description": "Entry names." }

Key Required Meaning
name yes The param’s PrettyName — the human label, and the tooltip title.
variableName no The identifier your code receives. Defaults to name. On a forged component this is the pin label.
nickname no A pin label for build tooling that compiles the source into a plugin. Never reaches a forged script component.
type yes The type-hint converter. See Type hints.
access yes item, list, or tree.
description no The param tooltip. Defaults to empty — but write one; a param with no tooltip is a defect.
optional no Whether the param may be left unwired. Defaults to true. Inputs only.
default no A declared default value. Inputs only. See Defaults.

Params end up in exactly the header’s order. Index-based body code and downstream tooling stay valid — the header is the index order.

Two labels, and which is which

This is the part worth reading twice, because Rhino’s two label slots on a script param do not mean what their names suggest. The pin label is the variable name — the identifier your RunScript receives is exactly the text Grasshopper draws on the pin. The other slot is a free-form human label that shows as the title of the param’s tooltip.

header key where it lands on a forged component
variableName the pin label, and the C# / Python identifier in your code
name the tooltip title

variableName defaults to name, so a param carrying only name collapses both: write {"name": "Radius", …} and the pin reads Radius and your code takes a Radius argument. That is what you want almost always. (The build-only nickname defaults from name too, and independently — never via variableName, which would let a terse pin label become your variable name.)

Reach for variableName in the two cases where one string can’t do both jobs:

  • the label you want isn’t a legal identifier — {"name": "Max. Radius", "variableName": "MaxRadius"}; the tooltip reads Max. Radius, your code takes MaxRadius;
  • an input and an output want the same label — they cannot share one variable, so {"name": "Keys", "variableName": "OutKeys"} gives you an OutKeys variable, an OutKeys pin, and a tooltip titled Keys (OutKeys).

Rules the forge warns about:

  • variableName must be an identifier (^[A-Za-z_][A-Za-z0-9_]*$), and on C# sources must not be a C# keyword. Rhino validates nothing here — a variable name of "Not An Identifier" is stored without complaint and then fails to compile — so this warning is the only guard.
  • variableName must be unique across inputs and outputs combined: every one of them becomes a variable in the same scope.
  • name and nickname must each be unique per side. Sharing a label between an input and an output is the feature; sharing one within a side gives you two identically-labelled pins.

access

access C# signature Meaning
item T a single value
list List<T> one branch
tree DataTree<T> multi-branch

An item- or list-access component under Grasshopper’s implicit iteration appends the iteration index to its output paths ({i} in ⇒ {i;0} out). If a component’s outputs must mirror its input tree, take tree and loop the branches yourself.

Type hints

type names the param’s Converter. Matching is case-insensitive; the same vocabulary works for C# and Python.

Hint Notes
bool, int, double, string primitives
object / none / blank No Type Hint — raw goo, no conversion
Guid, DateTime, Complex, Color .NET / GH types
Point3d (point), Vector3d (vector), Plane (plane) frames & vectors
Interval, UVInterval Domain and Domain²
Line, Circle, Arc, Polyline, Rectangle3d primitive curves
Curve (curve) any curve
Box, Transform, Point3dList
Mesh (mesh), Surface, Extrusion, SubD, Brep (brep) surfaces & solids
PointCloud, GeometryBase GeometryBase accepts any geometry
Hatch, TextDot, TextEntity, Leader annotations

An unknown hint falls back to No Type Hint and the forge logs a HINT WARNING — hint typos never fail silently.

Spell the C# name. Grasshopper names two converters differently per language: the C# double converter is float on Python, and string is str. The forge resolves either spelling onto a live param, so a Python header saying "type": "float" works — but double and string are the canonical spellings, and they are what the default vocabulary keys on. A "default" on a float param is rejected, with a message telling you to spell it double.

Python outputs are never hinted. On Python components an output hint is an active converter that breaks list outputs. The forge leaves Python outputs on No Type Hint (matching stock components); an output’s type serves as documentation there. C# outputs are write-only, so their hints are applied but harmless.


optional and default

Two input-side keys. Between them they let a script tell “the user wired false” from “nothing is wired at all” — which a bare bool input cannot.

optional

Defaults to true, so omitting it changes nothing. "optional": false makes Grasshopper require a value: an unwired param goes orange with Input parameter X failed to collect data and the component does not solve at all.

That last part is the trade. A non-optional unwired input stops SolveInstance from running, which is exactly what you asked for — but it also means no code on that component runs to explain itself.

default

A declared value the param starts with. It becomes the param’s persistent data — the internalized value Grasshopper hands the script when nothing is wired in. An unwired {"type": "bool", "default": true} input therefore reads true, which a plain unwired bool never could.

The vocabulary is bool, int, double, string, and nothing else — the types JSON has a scalar for. There is no JSON literal that spells a Point3d, so there is no way to declare one. Internalize such a value on the param itself instead — the forge leaves existing persistent data alone (see below), so it survives every re-forge.

The value’s JSON kind must agree with type. A whole number is accepted on a double param and arrives as 2.0, not as an int; everything else that disagrees is rejected, out loud, on both surfaces:

header verdict
"type": "bool", "default": "yes" default 'yes' is not true or false (bool param)
"type": "int", "default": 2.5 default 2.5 is not a whole number (int param)
"type": "int", "default": true default True is not a whole number (int param)
"type": "Point3d", "default": 0 default is only supported on bool/int/double/string, not 'Point3d'
"type": "double", "default": [0,0,1] default [0, 0, 1] is not a number (double param)
a default on an output an output has no default — nothing is ever collected into it
optional on an output 'optional' is an input-side idea — RegisterOutputParams has no Optional pass to mirror it

A rejected default is never applied; the param is left alone.

Re-forging does not clobber a user’s value

Persistent data is user-visible and user-editable (right-click ▸ Internalise data). The forge applies a declared default only when the param has no persistent data, and leaves an existing value alone. So a value someone internalised survives every subsequent re-forge — at the cost that the header cannot reset one. Clear it on the component if you want the header’s value back.


Multi-line tooltips

JSON’s own escapes, so \n inside a description string is a real newline by the time the parser hands it over:

{ "name": "Target", "type": "object", "access": "tree",
  "description": "Which component each branch updates:\n• a guid\n• the keyword name\n• null to create new" }

There is nothing header-specific to know here, which is the point: write the escape JSON already defines, and the tooltip on the canvas breaks where you said it does.


Icons

icon accepts three forms:

  • SVG path — rasterized to a 24×24 PNG automatically via macOS sips (reused until the SVG is newer);
  • PNG path — used directly (make it 24×24; GH draws at native pixel size);
  • embedded PNG — base64:<payload> or a full data:image/png;base64, URI, so the icon travels inside the source.

Path resolution: absolute paths are used as-is; relative paths resolve against the folder of the saved .gh — not the folder of the source file. The forge is handed source text and has no way to know where that text came from, so the document is the only anchor there is. Keep icons next to the .gh (e.g. in an icons/ subfolder beside it), or give an absolute path. In an unsaved document, relative icons are skipped with a warning; save the document and re-forge with Force to pick them up.

Icons are strictly best-effort. A missing file, bad extension, corrupt image, or SVG rasterization off macOS produces an icon warning: in the Log and the forge carries on — params, source, and other metadata are unaffected. On Windows, use a PNG path or a base64 icon (SVG-only icons are skipped there).


Language detection

The forge picks the language in this order:

  1. header language key (csharp / python);
  2. a #! python shebang in the first lines;
  3. the header comment style — /* @component ⇒ C#, """@component or # @component ⇒ Python;
  4. for headerless updates, the target component’s own type;
  5. headerless heuristics — Script_Instance or using X; ⇒ C#; a leading docstring or import ⇒ Python.

If none decide, the forge errors and asks for a language key. Add language whenever the language would otherwise be ambiguous.


Headerless sources

A source with no header is injected as-is: the forge creates a stock component (default x, y inputs / out, a outputs) or pushes into the target, touching no params, metadata, or icon.

A headerless create leans entirely on the heuristics above, so a marker-less body (e.g. a bare a = x + y) can’t be classified and errors — add a recognizable marker (import, a docstring, a using) or give it a header. A headerless update never hits this: the target’s own type settles the language.


Warnings

Header problems short of a JSON syntax error are reported in the Log output and never block the forge — the component is built, and the warning tells you what it could not honour. Read the Log when a forge doesn’t do what you expected; it is the whole diagnostic surface.

Log line Cause
DRIFT WARNING a variableName is declared in the header but missing from the RunScript signature, or vice versa — the header and your code disagree about the param list.
HINT WARNING a type wasn’t recognized; the param fell back to No Type Hint.
NAME WARNING a variableName is not an identifier, is a C# keyword, or is claimed by both an input and an output.
LABEL WARNING two params on the same side share a name or nickname.
DEFAULT WARNING a default the param can’t take — the table under default — or a param with no persistent-data slot to put one in.
OPTIONAL WARNING optional was declared on an output.
QUOTE WARNING a description or a label contains a ". Harmless on the canvas; flagged because it is awkward for tooling that reads the source back.
icon warning: … see Icons — always best-effort.
lost wire: … a removed param dropped its connections. A renamed param keeps them and logs renamed X -> Y (n wire(s) kept) instead.

DRIFT WARNING is the one worth wiring your attention to: the header is what builds the pins, and your RunScript signature is what receives them, so a disagreement means a param exists on exactly one of the two.


Complete example

/* @component
{
  "name":          "Remap Number",
  "nickname":      "Remap",
  "description":   "Remaps a number from one interval into another. Values outside the source interval are extrapolated, not clamped.",
  "icon":          "icons/remap-number.svg",
  "instanceGuid":  "3f2a9c10-8b41-4e6d-9a2f-1c7e5b0d4a88",

  "inputs": [
    { "name": "Value", "type": "double", "access": "item",
      "description": "The number to remap." },
    { "name": "Source", "type": "Interval", "access": "item", "optional": false,
      "description": "The interval the value currently lives in." },
    { "name": "Target", "type": "Interval", "access": "item",
      "description": "The interval to map into." },
    { "name": "Clamp", "type": "bool", "access": "item", "default": false,
      "description": "Hold the result inside Target instead of extrapolating." }
  ],

  "outputs": [
    { "name": "Mapped", "type": "double", "access": "item",
      "description": "The remapped number." }
  ]
}
*/

using System;
using Grasshopper.Kernel;

public class Script_Instance : GH_ScriptInstance
{
  private void RunScript(double Value, Rhino.Geometry.Interval Source,
    Rhino.Geometry.Interval Target, bool Clamp, out object Mapped)
  {
    double t = Source.NormalizedParameterAt(Value);
    if (Clamp) t = Math.Max(0.0, Math.Min(1.0, t));
    Mapped = Target.ParameterAt(t);
  }
}

Do you plan to put this project on GitHub? Would love to contribute to it for example add a skill for LLM to properly write the header

I’ve been considering sharing on GitHub, too. I appreciate your enthusiasm to build out the skill for writing the header.

Right now the source is entangled with a development kit including a companion tool that compiles a plugin from the same source files that the forge can turn into script components. Together they essentially allow for hot reloading plugin components during development which isn’t otherwise possible on macOS (in my limited experience).

All that is to say that I have an organizational challenge and fair amount of clean up before I would open up the source.

Hi @Jun_Wang

You can try out the ghost writer plugin! It does exactly what you says, convert script method input into component parameter description!

It reads the xml(c#) or docstring(python) method signature doc and automatically populate it onto corresponding component parameter. It also works directly in an external IDE like Cursor or VSCode, with built in agent instruction to create the xml doc.

@Anthony_Chu1, I would love to try Ghost Writer but after installing through the Package Manager, it shows a number of errors when opening Grasshopper. After that, Grasshopper can continue to open, but there is no Ghost writer component available that I can find. I’m on macOS.

Grasshopper Loading Errors

Object: CodeIDEPythonComponent (level 1)
{
The type initializer for ‘’ threw an exception.
TypeInitializationException
System.TypeInitializationException: The type initializer for ‘’ threw an exception.
—> System.DllNotFoundException: Unable to load shared library ‘kernel32.dll’ or one of its dependencies. In order to help diagnose loading problems, consider setting the DYLD_PRINT_LIBRARIES environment variable:
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file)
dlopen(kernel32.dll.dylib, 0x0001): tried: ‘kernel32.dll.dylib’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSkernel32.dll.dylib’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/kernel32.dll.dylib’ (no such file), ‘/usr/lib/kernel32.dll.dylib’ (no such file, not in dyld cache), ‘kernel32.dll.dylib’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file)
dlopen(libkernel32.dll.dylib, 0x0001): tried: ‘libkernel32.dll.dylib’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSlibkernel32.dll.dylib’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/libkernel32.dll.dylib’ (no such file), ‘/usr/lib/libkernel32.dll.dylib’ (no such file, not in dyld cache), ‘libkernel32.dll.dylib’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file)
dlopen(kernel32.dll, 0x0001): tried: ‘kernel32.dll’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSkernel32.dll’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/kernel32.dll’ (no such file), ‘/usr/lib/kernel32.dll’ (no such file, not in dyld cache), ‘kernel32.dll’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file)
dlopen(libkernel32.dll, 0x0001): tried: ‘libkernel32.dll’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSlibkernel32.dll’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/libkernel32.dll’ (no such file), ‘/usr/lib/libkernel32.dll’ (no such file, not in dyld cache), ‘libkernel32.dll’ (relative path not allowed in hardened program)

at SpecialFolderOptionsetBlockSize.CreateFileW(String filename, UInt32 access, UInt32 share, IntPtr securityAttributes, UInt32 creationDisposition, UInt32 flagsAndAttributes, IntPtr templateFile)
at StableGetTypeToken(String, UInt32, UInt32, IntPtr, UInt32, UInt32, IntPtr)
at .cctor()
— End of inner exception stack trace —
at System.RuntimeTypeHandle.GetActivationInfo(ObjectHandleOnStack pRuntimeType, * ppfnAllocator, Void** pvAllocatorFirstArg, * ppfnCtor, BOOL* pfCtorIsPublic)
at System.RuntimeType.ActivatorCache..ctor(RuntimeType rt)
at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean wrapExceptions)
at Grasshopper.Kernel.GH_ComponentServer.LoadDocObject(Type T, GH_AssemblyInfo assembly, GH_GuidConflictSolution& conflictSolution) in /Users/bozo/TeamCity/buildAgent/work/96e64af5b81c6f85/src4/rhino4/Plug-ins/Grasshopper/Grasshopper/GH_ComponentServerAddInLoading.vb:line 1409
}

Object: CodeIDEPythonComponent (level 2)
{
Unable to load shared library ‘kernel32.dll’ or one of its dependencies. In order to help diagnose loading problems, consider setting the DYLD_PRINT_LIBRARIES environment variable:
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file)
dlopen(kernel32.dll.dylib, 0x0001): tried: ‘kernel32.dll.dylib’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSkernel32.dll.dylib’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/kernel32.dll.dylib’ (no such file), ‘/usr/lib/kernel32.dll.dylib’ (no such file, not in dyld cache), ‘kernel32.dll.dylib’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file)
dlopen(libkernel32.dll.dylib, 0x0001): tried: ‘libkernel32.dll.dylib’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSlibkernel32.dll.dylib’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/libkernel32.dll.dylib’ (no such file), ‘/usr/lib/libkernel32.dll.dylib’ (no such file, not in dyld cache), ‘libkernel32.dll.dylib’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file)
dlopen(kernel32.dll, 0x0001): tried: ‘kernel32.dll’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSkernel32.dll’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/kernel32.dll’ (no such file), ‘/usr/lib/kernel32.dll’ (no such file, not in dyld cache), ‘kernel32.dll’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file)
dlopen(libkernel32.dll, 0x0001): tried: ‘libkernel32.dll’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSlibkernel32.dll’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/libkernel32.dll’ (no such file), ‘/usr/lib/libkernel32.dll’ (no such file, not in dyld cache), ‘libkernel32.dll’ (relative path not allowed in hardened program)

DllNotFoundException
System.DllNotFoundException: Unable to load shared library ‘kernel32.dll’ or one of its dependencies. In order to help diagnose loading problems, consider setting the DYLD_PRINT_LIBRARIES environment variable:
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file)
dlopen(kernel32.dll.dylib, 0x0001): tried: ‘kernel32.dll.dylib’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSkernel32.dll.dylib’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/kernel32.dll.dylib’ (no such file), ‘/usr/lib/kernel32.dll.dylib’ (no such file, not in dyld cache), ‘kernel32.dll.dylib’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file)
dlopen(libkernel32.dll.dylib, 0x0001): tried: ‘libkernel32.dll.dylib’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSlibkernel32.dll.dylib’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/libkernel32.dll.dylib’ (no such file), ‘/usr/lib/libkernel32.dll.dylib’ (no such file, not in dyld cache), ‘libkernel32.dll.dylib’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file)
dlopen(kernel32.dll, 0x0001): tried: ‘kernel32.dll’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSkernel32.dll’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/kernel32.dll’ (no such file), ‘/usr/lib/kernel32.dll’ (no such file, not in dyld cache), ‘kernel32.dll’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file)
dlopen(libkernel32.dll, 0x0001): tried: ‘libkernel32.dll’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSlibkernel32.dll’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/libkernel32.dll’ (no such file), ‘/usr/lib/libkernel32.dll’ (no such file, not in dyld cache), ‘libkernel32.dll’ (relative path not allowed in hardened program)

at SpecialFolderOptionsetBlockSize.CreateFileW(String filename, UInt32 access, UInt32 share, IntPtr securityAttributes, UInt32 creationDisposition, UInt32 flagsAndAttributes, IntPtr templateFile)
at StableGetTypeToken(String, UInt32, UInt32, IntPtr, UInt32, UInt32, IntPtr)
at .cctor()
}

Object: CodeIDECSharpComponent (level 1)
{
The type initializer for ‘’ threw an exception.
TypeInitializationException
System.TypeInitializationException: The type initializer for ‘’ threw an exception.
—> System.DllNotFoundException: Unable to load shared library ‘kernel32.dll’ or one of its dependencies. In order to help diagnose loading problems, consider setting the DYLD_PRINT_LIBRARIES environment variable:
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file)
dlopen(kernel32.dll.dylib, 0x0001): tried: ‘kernel32.dll.dylib’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSkernel32.dll.dylib’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/kernel32.dll.dylib’ (no such file), ‘/usr/lib/kernel32.dll.dylib’ (no such file, not in dyld cache), ‘kernel32.dll.dylib’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file)
dlopen(libkernel32.dll.dylib, 0x0001): tried: ‘libkernel32.dll.dylib’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSlibkernel32.dll.dylib’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/libkernel32.dll.dylib’ (no such file), ‘/usr/lib/libkernel32.dll.dylib’ (no such file, not in dyld cache), ‘libkernel32.dll.dylib’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file)
dlopen(kernel32.dll, 0x0001): tried: ‘kernel32.dll’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSkernel32.dll’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/kernel32.dll’ (no such file), ‘/usr/lib/kernel32.dll’ (no such file, not in dyld cache), ‘kernel32.dll’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file)
dlopen(libkernel32.dll, 0x0001): tried: ‘libkernel32.dll’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSlibkernel32.dll’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/libkernel32.dll’ (no such file), ‘/usr/lib/libkernel32.dll’ (no such file, not in dyld cache), ‘libkernel32.dll’ (relative path not allowed in hardened program)

at SpecialFolderOptionsetBlockSize.CreateFileW(String filename, UInt32 access, UInt32 share, IntPtr securityAttributes, UInt32 creationDisposition, UInt32 flagsAndAttributes, IntPtr templateFile)
at StableGetTypeToken(String, UInt32, UInt32, IntPtr, UInt32, UInt32, IntPtr)
at .cctor()
— End of inner exception stack trace —
at System.RuntimeTypeHandle.GetActivationInfo(ObjectHandleOnStack pRuntimeType, * ppfnAllocator, Void** pvAllocatorFirstArg, * ppfnCtor, BOOL* pfCtorIsPublic)
at System.RuntimeType.ActivatorCache..ctor(RuntimeType rt)
at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean wrapExceptions)
at Grasshopper.Kernel.GH_ComponentServer.LoadDocObject(Type T, GH_AssemblyInfo assembly, GH_GuidConflictSolution& conflictSolution) in /Users/bozo/TeamCity/buildAgent/work/96e64af5b81c6f85/src4/rhino4/Plug-ins/Grasshopper/Grasshopper/GH_ComponentServerAddInLoading.vb:line 1409
}

Object: CodeIDECSharpComponent (level 2)
{
Unable to load shared library ‘kernel32.dll’ or one of its dependencies. In order to help diagnose loading problems, consider setting the DYLD_PRINT_LIBRARIES environment variable:
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file)
dlopen(kernel32.dll.dylib, 0x0001): tried: ‘kernel32.dll.dylib’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSkernel32.dll.dylib’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/kernel32.dll.dylib’ (no such file), ‘/usr/lib/kernel32.dll.dylib’ (no such file, not in dyld cache), ‘kernel32.dll.dylib’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file)
dlopen(libkernel32.dll.dylib, 0x0001): tried: ‘libkernel32.dll.dylib’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSlibkernel32.dll.dylib’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/libkernel32.dll.dylib’ (no such file), ‘/usr/lib/libkernel32.dll.dylib’ (no such file, not in dyld cache), ‘libkernel32.dll.dylib’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file)
dlopen(kernel32.dll, 0x0001): tried: ‘kernel32.dll’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSkernel32.dll’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/kernel32.dll’ (no such file), ‘/usr/lib/kernel32.dll’ (no such file, not in dyld cache), ‘kernel32.dll’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file)
dlopen(libkernel32.dll, 0x0001): tried: ‘libkernel32.dll’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSlibkernel32.dll’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/libkernel32.dll’ (no such file), ‘/usr/lib/libkernel32.dll’ (no such file, not in dyld cache), ‘libkernel32.dll’ (relative path not allowed in hardened program)

DllNotFoundException
System.DllNotFoundException: Unable to load shared library ‘kernel32.dll’ or one of its dependencies. In order to help diagnose loading problems, consider setting the DYLD_PRINT_LIBRARIES environment variable:
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll.dylib’ (no such file)
dlopen(kernel32.dll.dylib, 0x0001): tried: ‘kernel32.dll.dylib’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSkernel32.dll.dylib’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/kernel32.dll.dylib’ (no such file), ‘/usr/lib/kernel32.dll.dylib’ (no such file, not in dyld cache), ‘kernel32.dll.dylib’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll.dylib’ (no such file)
dlopen(libkernel32.dll.dylib, 0x0001): tried: ‘libkernel32.dll.dylib’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSlibkernel32.dll.dylib’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/libkernel32.dll.dylib’ (no such file), ‘/usr/lib/libkernel32.dll.dylib’ (no such file, not in dyld cache), ‘libkernel32.dll.dylib’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/kernel32.dll’ (no such file)
dlopen(kernel32.dll, 0x0001): tried: ‘kernel32.dll’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSkernel32.dll’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/kernel32.dll’ (no such file), ‘/usr/lib/kernel32.dll’ (no such file, not in dyld cache), ‘kernel32.dll’ (relative path not allowed in hardened program)
dlopen(/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll, 0x0001): tried: ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file), ‘/System/Volumes/Preboot/Cryptexes/OS/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file), ‘/Users/user/Library/Application Support/McNeel/Rhinoceros/packages/8.0/GhostWriter/0.5.22.7494/net8.0/libkernel32.dll’ (no such file)
dlopen(libkernel32.dll, 0x0001): tried: ‘libkernel32.dll’ (relative path not allowed in hardened program), ‘/System/Volumes/Preboot/Cryptexes/OSlibkernel32.dll’ (no such file), ‘/Applications/Rhino 8.app/Contents/Frameworks/libkernel32.dll’ (no such file), ‘/usr/lib/libkernel32.dll’ (no such file, not in dyld cache), ‘libkernel32.dll’ (relative path not allowed in hardened program)

at SpecialFolderOptionsetBlockSize.CreateFileW(String filename, UInt32 access, UInt32 share, IntPtr securityAttributes, UInt32 creationDisposition, UInt32 flagsAndAttributes, IntPtr templateFile)
at StableGetTypeToken(String, UInt32, UInt32, IntPtr, UInt32, UInt32, IntPtr)
at .cctor()
}

The C# component does this automatically… not sure why the Python doesn’t. I also spend a lot of time manually setting input names, Item/List/Tree access and geometry hint.