GNOME Shell Extensions Review Guidelines
TIP
If this is your first time writing an extension, please see the Getting Started guide.
These are the guidelines for developers who would like their extensions distributed on the extensions.gnome.org. Extensions are reviewed carefully for malicious code, malware and security risks, but not for bugs.
General Guidelines
There are three basic guidelines for the operation of an extension:
- Don't create or modify anything before
enable()
is called - Use
enable()
to create objects, connect signals and add main loop sources - Use
disable()
to cleanup anything done inenable()
These general tips will help your extension pass review:
- Write clean code, with consistent indentation and style
- Use modern features like ES6 classes and
async
/await
- Use a linter to check for logic and syntax errors
- Ask other developers for advice on Matrix
Rules
Only use initialization for static resources
Extensions MUST NOT create any objects, connect any signals, add any main loop sources or modify GNOME Shell during initialization.
In GNOME 45 and later, initialization happens when extension.js
is imported and the Extension
class is constructed (e.g. constructor()
is called).
GNOME Shell 45 and later
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
export default class ExampleExtension extends Extension {
constructor(metadata) {
super(metadata);
// DO NOT create objects, connect signals or add main loop sources here
}
enable() {
// Create objects, connect signals, create main loop sources, etc.
}
disable() {
// Destroy objects, disconnect signals, remove main loop sources, etc.
}
}
In GNOME 44 and earlier, initialization happens when extension.js
is imported and the init()
function is called. When using the Extension
class pattern, the constructor()
is also called.
GNOME Shell 44 and earlier
const ExtensionUtils = imports.misc.extensionUtils;
class Extension {
constructor() {
// DO NOT create objects, connect signals or add main loop sources here
}
enable() {
// Create objects, connect signals, create main loop sources, etc.
}
disable() {
// Destroy objects, disconnect signals, remove main loop sources, etc.
}
}
function init() {
// Initialize translations before returning the extension object
ExtensionUtils.initTranslations();
return new Extension();
}
Extensions MAY create static data structures and instances of built-in JavaScript objects (such as Regexp()
and Map()
), but all dynamically stored memory must be cleared or freed in disable()
(e.g. Map.prototype.clear()
). All GObject classes, such as Gio.Settings
and St.Widget
are disallowed.
Static Objects
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
// Extensions **MAY** construct built-in JavaScript types in the module scope
const FoobarCache = new Map();
// Extensions **MAY** create static data structures in the module scope
const FoobarState = {
DEFAULT: 0,
CHANGED: 1,
};
class Foobar {
state = FoobarState.DEFAULT;
}
let DEFAULT_FOOBAR = null;
export default class ExampleExtension extends Extension {
constructor(metadata) {
super(metadata);
// Extensions **MAY** create and store a reasonable amount of static
// data during initialization
this._state = {
enabled: false,
};
}
enable() {
// Extensions **MAY** construct instances of classes and assign them
// to variables in the module scope
if (DEFAULT_FOOBAR === null)
DEFAULT_FOOBAR = new Foobar();
// Extensions **MAY** dynamically store data in the module scope
for (let i = 0; i < 10; i++)
FoobarCache.set(`${i}`, new Date());
this._state.enabled = true;
}
disable() {
// Extensions **MUST** destroy instances of classes assigned to
// variables in the module scope
if (DEFAULT_FOOBAR instanceof Foobar)
DEFAULT_FOOBAR = null;
// Extensions **MUST** clear dynamically stored data in the module scope
FoobarCache.clear();
this._state.enabled = false;
}
}
Destroy all objects
Any objects or widgets created by an extension MUST be destroyed in disable()
.
GNOME 45 and later
import St from 'gi://St';
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
export default class ExampleExtension extends Extension {
enable() {
this._widget = new St.Widget();
}
disable() {
this._widget?.destroy();
this._widget = null;
}
}
GNOME 44 and earlier
const St = imports.gi.St;
class Extension {
enable() {
this._widget = new St.Widget();
}
disable() {
if (this._widget) {
this._widget.destroy();
this._widget = null;
}
}
}
function init() {
return new Extension();
}
Disconnect all signals
Any signal connections made by an extension MUST be disconnected in disable()
:
GNOME 45 and later
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
export default class ExampleExtension extends Extension {
enable() {
this._handlerId = global.settings.connect('changed::favorite-apps', () => {
console.log('app favorites changed');
});
}
disable() {
if (this._handlerId) {
global.settings.disconnect(this._handlerId);
this._handlerId = null;
}
}
}
GNOME 44 and earlier
class Extension {
enable() {
this._handlerId = global.settings.connect('changed::favorite-apps', () => {
console.log('app favorites changed');
});
}
disable() {
if (this._handlerId) {
global.settings.disconnect(this._handlerId);
this._handlerId = null;
}
}
}
function init() {
return new Extension();
}
Remove main loop sources
Any main loop sources created MUST be removed in disable()
.
Note that all sources MUST be removed in disable()
, even if the callback function will eventually return false
or GLib.SOURCE_REMOVE
.
GNOME 45 and later
import GLib from 'gi://GLib';
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
export default class ExampleExtension extends Extension {
enable() {
this._sourceId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 5, () => {
console.log('Source triggered');
return GLib.SOURCE_CONTINUE;
});
}
disable() {
if (this._sourceId) {
GLib.Source.remove(this._sourceId);
this._sourceId = null;
}
}
}
GNOME 44 and earlier
const GLib = imports.gi.GLib;
class Extension {
enable() {
this._sourceId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 5, () => {
console.log('Source triggered');
return GLib.SOURCE_CONTINUE;
});
}
disable() {
if (this._sourceId) {
GLib.Source.remove(this._sourceId);
this._sourceId = null;
}
}
}
function init() {
return new Extension();
}
Do not use deprecated modules
Extensions MUST NOT import deprecated modules.
Deprecated Module | Replacement |
---|---|
ByteArray | TextDecoder and TextEncoder |
Lang | ES6 Classes and Function.prototype.bind() |
Mainloop | GLib.MainLoop , GLib.timeout_add() , etc |
Do not import GTK libraries in GNOME Shell
Extensions MUST NOT import Gdk
, Gtk
or Adw
in the GNOME Shell process.
These libraries are only for use in the preferences process (prefs.js
) and will conflict with Clutter
and other libraries used by GNOME Shell.
Do not import GNOME Shell libraries in Preferences
Extensions MUST NOT import Clutter
, Meta
, St
or Shell
in the preferences process.
These libraries are only for use in the GNOME Shell process (extension.js
) and will conflict with Gtk
and other libraries.
Avoid interfering with the Extension System
Extensions which modify, reload or interact with other extensions or the extension system are generally discouraged.
While not strictly prohibited, these extensions will be reviewed on a case-by-case basis and may be rejected at the reviewer's discretion.
Code must not be obfuscated
Extension code MUST be readable and reviewable JavaScript.
A specific code-style is not enforced, however submitted code must be formatted in a way that can be easily reviewed. The following rules MUST be adhered to:
- JavaScript code must be readable and reasonably structured
- JavaScript code must not be minified or obfuscated
- TypeScript must be transpiled to well-formatted JavaScript
No excessive logging
Extension MUST NOT print excessively to the log. The log should only be used for important messages and errors.
If a reviewer determines that an extension is writing excessively to the log, the extension will be rejected.
Extensions should not force dispose a GObject
Extensions SHOULD NOT call GObject.Object.run_dispose()
unless absolutely necessary.
If absolutely necessary, any call to this method MUST have a comment explaining the real-world situation that makes it a requirement.
Scripts and Binaries
Use of external scripts and binaries is strongly discouraged. In cases where this is unavoidable for the extension to serve it's purpose, the following rules must be adhered to:
- Extensions MUST NOT include binary executables or libraries
- Processes MUST be spawned carefully and exit cleanly
- Scripts MUST be written in GJS, unless absolutely necessary
- Scripts must be distributed under an OSI approved license
Reviewing Python modules, HTML, and web JavaScript dependencies is out of scope for extensions.gnome.org. Unless required functionality is only available in another scripting language, scripts must be written in GJS.
Extensions may install modules from well-known services such as pip
, npm
or yarn
but MUST require explicit user action. For example, the extension preferences may include a page which describes the modules to be installed with a button.
Privileged Subprocess must not be user-writable
Spawning privileged subprocesses should be avoided at all costs.
If absolutely necessary, the subprocess MUST be run with pkexec
and MUST NOT be an executable or script that can be modified by a user process.
Extensions must be functional
Extensions are reviewed, but not always tested for functionality so an extension MAY be approved with broken functionality or inoperable preferences window.
However, if an extension is tested and found to be fundamentally broken it will be rejected. Extensions which serve no purpose or have no functionality will also be rejected.
metadata.json must be well-formed
The metadata.json
file that ships with every extension should be well-formed and accurately reflect the extension.
Key | Description |
---|---|
name | This should not conflict with another extension if possible. If it is a fork of another extension it MUST have a unique name to distinguish it. |
uuid | This must be of the form extension-id@user-domain . extension-id and user-domain MUST only contain numbers, letters, period (. ), underscore (_ ) and dash (- ). user-domain MUST be a domain or namespace you control and be separated from extension-id with an @ symbol. |
description | This should be a reasonable length, but may contain a few paragraphs separated with \n literals or a bullet point list made with * characters. |
version | Deprecated: This field is set for internal use by extensions.gnome.org . |
shell-version | This MUST only contain stable releases and up to one development release. Extensions must not claim to support future GNOME Shell versions. As of GNOME 40, an entry may simply be a major version like 40 to cover the entire release. |
url | This should be a link to a Github or GitLab repository where users can report problems and learn more about your extension. |
session-modes | This MUST be dropped if you are only using user mode. The only valid values are user and unlock-dialog . |
donations | This MUST only contain possible keys and MUST be dropped if you don't use any of the keys. |
Example:
{
"uuid": "color-button@my-account.github.io",
"name": "ColorButton",
"description": "ColorButton adds a colored button to the panel.\n\nIt is a fork of MonochromeButton.",
"shell-version": [ "3.38", "40", "41.alpha" ],
"url": "https://github.com/my-account/color-button",
"session-modes": [ "unlock-dialog", "user" ]
}
Session Modes
In rare cases, it is necessary for an extension to continue running while the screen is locked. In order to be approved to use the unlock-dialog
session mode:
- It MUST be necessary for the extension to operate correctly.
- All signals related to keyboard events MUST be disconnected in
unlock-dialog
session mode. - The
disable()
function MUST have a comment explaining why you are usingunlock-dialog
.
Extensions MUST NOT disable selectively.
GSettings Schemas
For extensions that include a GSettings Schema:
- The Schema ID MUST use
org.gnome.shell.extensions
as a base ID. - The Schema path MUST use
/org/gnome/shell/extensions
as a base path. - The Schema XML file MUST be included in the extension ZIP file.
- The Schema XML filename MUST follow pattern of
<schema-id>.gschema.xml
.
Legal Restrictions
Licensing
GNOME Shell is licensed under the terms of the GPL-2.0-or-later
, which means that derived works like extensions MUST be distributed under compatible terms (eg. GPL-2.0-or-later
, GPL-3.0-or-later
).
While your extension may include code licensed under a permissive license such as BSD/MIT, you are still approving GNOME to distribute it under terms compatible with the GPL-2.0-or-later
.
If your extension contains code from another extension it MUST include attribution to the original author in the distributed files. Not doing so is a license violation and your extension will be rejected.
Copyrights and Trademarks
Extensions MUST NOT include copyrighted or trademarked content without proof of express permission from the owner. Examples include:
- Brand Names and Phrases
- Logos and Artwork
- Audio, Video or Multimedia
Recommendations
Don't include unnecessary files
Extension submissions should not include files that are not necessary for it to function. Examples include:
- build or install scripts
- .po and .pot files
- unused icons, images or other media
A reviewer MAY decide to reject an extension which includes an unreasonable amount of unnecessary data.
Use a linter
Using ESLint to check your code can catch syntax errors and other mistakes before submission, as well as enforce consistent code style. You can find the ESLint rules used by GNOME Shell on GitLab.
Following a specific code style is not a requirement for approval, but if the codebase of an extension is too messy to properly review it MAY be rejected. This includes obfuscators and transpilers used with TypeScript.
UI Design
Although not required for approval, it is recommended that extension preferences follow the GNOME Human Interface Guidelines to improve consistency with the GNOME desktop.
Getting Help
There are several ways for you to get help with your extension.
- Ask in the extensions Matrix room
- Ask on discourse.gnome.org using the extensions tag
- Ask on StackOverflow using the gnome-shell-extensions and/or gjs tags