Running scripts in gcontext
gcontext stores the structure (scripts, docs, secret NAMES) in the cloud; execution and
secret VALUES stay on the user's machine. tool_run_script runs Python locally via uv run
with secret values injected into the environment from ~/.gcontext/.env (chmod 0600; OS
keychain opt-in), so any machine with its secrets set runs the same script identically. Output is
scrubbed of known secret values before it reaches the agent. Runs are capped at 300 seconds.
tool_run_shell(command) runs a shell command under the same rules (secret env injected,
output scrubbed); use it for git, curl, or CLI tools, and tool_run_script for anything
with real logic.
Three layers, promote upward
| Layer | When | How |
|---|---|---|
| Inline code | One-off exploration, disposable | tool_run_script(code="...") |
| Stored script | Anything worth running twice | Write to the workspace, run with tool_run_script(path="/...py", args=[...]) |
| Command | A human triggers it by name | /_commands/<name>.md + /_commands/<name>.py |
Promotion is the workflow: an inline script that proved useful gets a PEP 723 header and a
workspace path. A stored script that gets asked for repeatedly gets a command .md.
Dependency resolution
tool_run_script resolves dependencies automatically, in this order, all merged:
modules=["stripe"]: each named folder'smodule.yamldependencies:list.- Running by
path: the script's own folder'smodule.yaml, inherited automatically. - A PEP 723
# /// scriptheader in the code itself.
/_commands/ has no module.yaml, so scripts there MUST carry a PEP 723 header.
Versions pass through verbatim (stripe==11.* works); pin what matters.
Script conventions
- Deps: declare in a PEP 723
# /// scriptheader or the module'smodule.yaml(see above). - Secrets:
os.environ["NAME"]only, never hardcoded, never printed. The names come from the secret registry (see/_docs/secrets.md); values live in~/.gcontext/.envon the user's machine. - Args:
sys.argv[1:], main identifier first. Print a usage line and exit 1 when missing. - Output: markdown to stdout, the agent relays it verbatim. Exit 0 on success.
- Resilience: wrap each per-item external call (one Stripe account, one API page) in try/except that prints a warning line and continues. One bad item must not kill the run, crashes cost the agent a whole edit-and-retry loop.
- Safety: read-only by default, using read-only keys where they exist. A script that writes says so in its docstring and its name.
Command conventions (/_commands/<name>.md)
The connector exposes every /_commands/<name>.md as the slash command /<name>. Keep the
.md to UX only; logic lives in the script. Body edits are live; a NEW file needs a client
reconnect (clients cache the prompt list).
- Frontmatter
description:, shown in the client's command list. {{input}}in the body is the only substitution the connector performs; it also makes the command declare its argument.$ARGUMENTSdoes nothing.- One execution step:
tool_run_script(path="/_commands/<name>.py", args=["<input>"]), never "read the script and copy it to /tmp"; the path call is one round trip and the script body never enters the agent's context. - Relay instruction: output is markdown, relay verbatim (not in a code block), then at most a few lines of judgment.
- Self-repair clause: if the script errors, fix it and write the corrected version back to the workspace so the stored copy stays good.
Reference implementation: /_commands/find-member.md + /_commands/find-member.py in the
workspace.