js-gnome-extensions
SKILL.md
GNOME Shell Extensions
Build extensions for GNOME Shell 45+ using GJS with ESModules.
Key Resources
- Public APIs (GJS Docs): https://gjs-docs.gnome.org/
- GNOME Shell UI Source: https://gitlab.gnome.org/GNOME/gnome-shell/-/tree/main/js/ui
- Extensions Guide: https://gjs.guide/extensions/
- Review Guidelines: https://gjs.guide/extensions/review-guidelines/review-guidelines.html
Architecture Overview
Extensions run inside the gnome-shell process using Clutter/St toolkits (not GTK).
Preferences run in a separate GTK4/Adwaita process.
Library stack (bottom-up):
- Clutter — Actor-based toolkit, layout managers, animations
- St — Shell Toolkit: buttons, icons, labels, entries, scroll views (CSS-styleable)
- Meta (Mutter) — displays, workspaces, windows, keybindings
- Shell —
globalobject, app tracking, utilities - js/ui/ — GNOME Shell JS modules (Main, Panel, PopupMenu, QuickSettings, etc.)
Import conventions in extension.js:
import Clutter from "gi://Clutter";
import GObject from "gi://GObject";
import Gio from "gi://Gio";
import GLib from "gi://GLib";
import Meta from "gi://Meta";
import Shell from "gi://Shell";
import St from "gi://St";
import {
Extension,
gettext as _,
} from "resource:///org/gnome/shell/extensions/extension.js";
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import * as PanelMenu from "resource:///org/gnome/shell/ui/panelMenu.js";
import * as PopupMenu from "resource:///org/gnome/shell/ui/popupMenu.js";
import * as QuickSettings from "resource:///org/gnome/shell/ui/quickSettings.js";
Import conventions in prefs.js:
import Gdk from "gi://Gdk?version=4.0";
import Gtk from "gi://Gtk?version=4.0";
import Adw from "gi://Adw";
import Gio from "gi://Gio";
import {
ExtensionPreferences,
gettext as _,
} from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
Critical rule: Never import GTK/Gdk/Adw in extension.js. Never import Clutter/Meta/St/Shell in prefs.js.
Extension Lifecycle
Required Files
metadata.json— UUID, name, description, shell-version, urlextension.js— Default export: subclass ofExtension
Optional Files
prefs.js— Subclass ofExtensionPreferences(GTK4/Adwaita)stylesheet.css— CSS for St widgets in gnome-shell (not prefs)schemas/— GSettings schema XML + compiled binarylocale/— Gettext translation .mo files
enable()/disable() Contract
import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";
export default class MyExtension extends Extension {
enable() {
// Create objects, connect signals, add UI
}
disable() {
// MUST undo everything done in enable():
// - Destroy all created widgets
// - Disconnect all signals
// - Remove all GLib.timeout/idle sources
// - Null out all references
}
}
Rules (enforced by extension review):
- Do NOT create GObject instances or connect signals in
constructor() constructor()may only set up static data (RegExp, Map, etc.) and callsuper(metadata)- Everything created in
enable()MUST be cleaned up indisable() disable()is called on lock screen (unlesssession-modesincludesunlock-dialog)
Reference Files
Detailed documentation is split into reference files. Read the appropriate file based on the task:
- references/review-guidelines.md — Complete review rules for extensions.gnome.org submission. Read before submitting or when reviewing extension code for compliance.
- references/ui-patterns.md — Panel indicators, popup menus, quick settings (toggles, sliders, menus), dialogs, notifications, and search providers with complete code examples. Read when building any UI component.
- references/preferences.md — GSettings schemas, prefs.js with GTK4/Adwaita, settings binding. Read when implementing extension preferences.
- references/development.md — Getting started, testing, debugging, translations, InjectionManager, and packaging. Read when setting up a new extension or debugging.
- references/porting-guide.md — Breaking changes for GNOME Shell 45–49. Read when porting an extension to a newer version.
Quick Start: Panel Indicator Extension
Minimal working extension with a panel icon:
metadata.json
{
"uuid": "my-extension@example.com",
"name": "My Extension",
"description": "Does something useful",
"shell-version": ["47", "48", "49"],
"url": "https://github.com/user/my-extension"
}
extension.js
import St from "gi://St";
import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import * as PanelMenu from "resource:///org/gnome/shell/ui/panelMenu.js";
export default class MyExtension extends Extension {
enable() {
this._indicator = new PanelMenu.Button(0.0, this.metadata.name, false);
const icon = new St.Icon({
icon_name: "face-laugh-symbolic",
style_class: "system-status-icon",
});
this._indicator.add_child(icon);
Main.panel.addToStatusArea(this.uuid, this._indicator);
}
disable() {
this._indicator?.destroy();
this._indicator = null;
}
}
Install & Test
# Create extension directory
mkdir -p ~/.local/share/gnome-shell/extensions/my-extension@example.com
# Copy files there, then:
# Wayland: run nested session
dbus-run-session gnome-shell --devkit --wayland # GNOME 49+
dbus-run-session gnome-shell --nested --wayland # GNOME 48 and earlier
# Enable extension in nested session
gnome-extensions enable my-extension@example.com
# Watch logs
journalctl -f -o cat /usr/bin/gnome-shell
Common Patterns Cheatsheet
Connect a signal (and clean up)
enable() {
this._handlerId = someObject.connect('some-signal', () => { /* ... */ });
}
disable() {
if (this._handlerId) {
someObject.disconnect(this._handlerId);
this._handlerId = null;
}
}
Add a timeout (and clean up)
enable() {
this._timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 5, () => {
// do work
return GLib.SOURCE_CONTINUE; // or GLib.SOURCE_REMOVE
});
}
disable() {
if (this._timeoutId) {
GLib.Source.remove(this._timeoutId);
this._timeoutId = null;
}
}
Use GSettings
enable() {
this._settings = this.getSettings(); // uses metadata settings-schema
this._settings.bind('show-indicator', this._indicator, 'visible',
Gio.SettingsBindFlags.DEFAULT);
}
disable() {
this._settings = null;
}
Override a method (InjectionManager)
import {
Extension,
InjectionManager,
} from "resource:///org/gnome/shell/extensions/extension.js";
import { Panel } from "resource:///org/gnome/shell/ui/panel.js";
export default class MyExtension extends Extension {
enable() {
this._injectionManager = new InjectionManager();
this._injectionManager.overrideMethod(
Panel.prototype,
"toggleCalendar",
(originalMethod) => {
return function (...args) {
console.debug("Calendar toggled!");
originalMethod.call(this, ...args);
};
},
);
}
disable() {
this._injectionManager.clear();
this._injectionManager = null;
}
}
Packaging for Submission
cd ~/.local/share/gnome-shell/extensions/my-extension@example.com
gnome-extensions pack --podir=po --extra-source=utils.js .
# GNOME 49+: upload directly
gnome-extensions upload --accept-tos
Weekly Installs
7
Repository
padparadscho/skillsFirst Seen
Feb 7, 2026
Security Audits
Installed on
amp7
opencode7
kimi-cli7
codex7
github-copilot7
gemini-cli7