Overview
This guide walks through building a complete ArgentOS plugin. Plugins can register tools, read service keys, persist configuration, and hook into the agent lifecycle — all without modifying core files.
Plugin Structure
~/.argentos/extensions/my-plugin/
├── argent.plugin.json # Manifest (required)
└── index.ts # Plugin entry point
Plugins are discovered from two locations:
Global : ~/.argentos/extensions/<plugin-id>/
Workspace : .argent/extensions/<plugin-id>/ (project-local)
The entry point is loaded by Jiti (TypeScript JIT), so .ts files work without a build step.
Manifest
Every plugin needs an argent.plugin.json for discovery and config validation:
{
"id" : "my-plugin" ,
"name" : "My Plugin" ,
"description" : "What this plugin does" ,
"version" : "1.0.0" ,
"configSchema" : {
"type" : "object" ,
"additionalProperties" : false ,
"properties" : {
"apiKey" : {
"type" : "string" ,
"description" : "API key (prefer service-keys.json)"
},
"userId" : {
"type" : "number" ,
"description" : "Linked user ID"
}
}
},
"uiHints" : {
"apiKey" : { "sensitive" : true , "label" : "API Key" }
}
}
Field Required Description idYes Unique plugin identifier configSchemaYes JSON Schema for plugin config (even if empty) nameNo Display name descriptionNo Short summary versionNo Semver version uiHintsNo UI rendering hints (sensitive flags, labels, placeholders) kindNo Plugin kind (e.g., "memory") channelsNo Channel IDs this plugin registers providersNo Provider IDs this plugin registers skillsNo Skill directories to load
Entry Point
Export a default function that receives the registration API:
export default function register ( api : any ) {
api . registerTool ({ /* ... */ });
api . on ( "before_agent_start" , async ( ctx : any ) => { /* ... */ });
}
No Core Imports
Plugins in ~/.argentos/extensions/ don’t have access to core node_modules. Use plain JSON Schema objects instead of @sinclair/typebox.
// Don't: import { Type } from "@sinclair/typebox"
// Do: plain JSON Schema
const schema = {
type: "object" as const ,
properties: {
query: { type: "string" as const , description: "Search query" },
limit: { type: "number" as const , description: "Max results" },
},
required: [ "query" ] as const ,
};
api . registerTool ({
name: "my_tickets" ,
description: "List support tickets" ,
parameters: {
type: "object" ,
properties: {
status: {
type: "string" ,
enum: [ "open" , "pending" , "resolved" ],
description: "Filter by status" ,
},
},
},
async execute ( _id : string , params : any ) {
const data = await fetchTickets ( params );
return {
content: [{ type: "text" , text: JSON . stringify ( data , null , 2 ) }],
};
},
});
For tools that require credentials or have side effects, mark them as optional:
api . registerTool (
{ name: "dangerous_tool" , /* ... */ },
{ optional: true },
);
Service Keys
ArgentOS has a centralized key store at ~/.argentos/service-keys.json, managed through the dashboard (Settings > API Keys). Plugins should read from this store rather than requiring separate config.
Reading Service Keys
function resolveApiKey ( variableName : string ) : string | undefined {
try {
const fs = require ( "node:fs" );
const path = require ( "node:path" );
const keysPath = path . join (
process . env . HOME ?? "/tmp" ,
".argentos" ,
"service-keys.json" ,
);
const raw = fs . readFileSync ( keysPath , "utf-8" );
const store = JSON . parse ( raw );
const entry = ( store . keys ?? []). find (
( k : any ) => k . variable === variableName && k . enabled !== false ,
);
if ( entry ?. value ) return entry . value ;
} catch {}
return process . env [ variableName ]; // Fallback for CI/servers
}
How It Works
User adds the key
Through Dashboard > Settings > API Keys .
Dashboard writes to store
Dashboard writes to ~/.argentos/service-keys.json.
Plugin reads at runtime
Plugin reads the key at runtime via the pattern above.
Fallback for CI
Falls back to process.env for CI/server deployments.
{
"keys" : [
{
"variable" : "MY_API_KEY" ,
"value" : "sk-..." ,
"enabled" : true ,
"label" : "My Service" ,
"category" : "other"
}
]
}
Config Persistence
Plugins can persist configuration to ~/.argentos/argent.json under plugins.entries.<id>.config:
function updatePluginConfig (
pluginId : string ,
key : string ,
value : any ,
) : boolean {
try {
const fs = require ( "node:fs" );
const path = require ( "node:path" );
const configPath = path . join (
process . env . HOME ?? "/tmp" ,
".argentos" ,
"argent.json" ,
);
const raw = fs . readFileSync ( configPath , "utf-8" );
const config = JSON . parse ( raw );
if ( ! config . plugins ) config . plugins = {};
if ( ! config . plugins . entries ) config . plugins . entries = {};
if ( ! config . plugins . entries [ pluginId ])
config . plugins . entries [ pluginId ] = { enabled: true };
if ( ! config . plugins . entries [ pluginId ]. config )
config . plugins . entries [ pluginId ]. config = {};
config . plugins . entries [ pluginId ]. config [ key ] = value ;
fs . writeFileSync (
configPath ,
JSON . stringify ( config , null , 2 ) + " \n " ,
"utf-8" ,
);
return true ;
} catch {
return false ;
}
}
This enables self-service setup flows where the agent discovers and saves config automatically.
Config in argent.json
{
"plugins" : {
"allow" : [ "my-plugin" ],
"entries" : {
"my-plugin" : {
"enabled" : true ,
"config" : {
"userId" : 12345
}
}
}
}
}
Plugin Allowlist
If plugins.allow[] has any entries, only plugins in that list are enabled. Non-bundled plugins default to enabled when the allowlist is empty. If your plugin isn’t loading, check whether an allowlist exists and add your plugin ID.
Lifecycle Hooks
before_agent_start
Fires before each agent run. Inject context or nudge the agent:
api . on ( "before_agent_start" , async ( ctx : any ) => {
const apiKey = resolveApiKey ( "MY_API_KEY" );
if ( ! apiKey ) return ;
const summary = await fetchSummary ( apiKey );
ctx . systemPromptSuffix = `
## My Integration
${ summary }
` . trim ();
});
Use systemPromptSuffix for:
Open ticket counts or active alerts
Nudging the agent when setup is incomplete
Injecting operator-specific context
Tips
Jiti loads TypeScript directly.
Don’t create mega-tools — split tickets, devices, and alerts into separate tools.
Return a helpful message if the API key is missing, don’t throw.
Use AbortSignal.timeout()
Prevent hung API calls from blocking the agent.
Users manage keys through the dashboard UI.
Let the agent discover configuration instead of requiring manual entry.