diff --git a/README.md b/README.md
index ac90e6f..6923060 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,8 @@ The `scratch-analysis` module will return an object containing high-level summar
 | `scripts`         | `count`                                                  |
 | `blocks`          | `count`, `unique`, `list`, `frequency`                   |
 | `sprites`         | `count`                                                  |
-| `variables`       | `count`                                                  |
+| `variables`       | `count`, `id`                                            |
+| `cloud`           | `count`, `id`                                            |
 | `lists`           | `count`                                                  |
 | `costumes`        | `count`, `list`, `hash`                                  |
 | `sounds`          | `count`, `list`, `hash`                                  |
diff --git a/lib/sb2.js b/lib/sb2.js
index 4831957..229a581 100644
--- a/lib/sb2.js
+++ b/lib/sb2.js
@@ -92,6 +92,24 @@ const blocks = function (project) {
     // Storage objects
     const result = [];
 
+    /**
+     * Determine if a argument is the name of a known cloud variable.
+     * @param  {string} arg Argument (variable name)
+     * @return {boolean}    Is cloud variable?
+     */
+    const isArgCloudVar = function (arg) {
+        // Validate argument
+        // @note "Hacked" inputs here could be objects (arrays)
+        if (typeof arg !== 'string') return false;
+
+        // Iterate over global variables and check to see if arg matches
+        for (let i in project.variables) {
+            const variable = project.variables[i];
+            if (variable.name === arg && variable.isPersistent) return true;
+        }
+        return false;
+    };
+
     /**
      * Walk scripts array(s) and build block list.
      * @param  {array} stack Stack of blocks
@@ -108,11 +126,20 @@ const blocks = function (project) {
                 continue;
             }
 
+            // Get opcode and check variable manipulation for the presence of
+            // cloud variables
+            let opcode = stack[i][0];
+            if (opcode === 'setVar:to:' || opcode === 'changeVar:by:') {
+                if (isArgCloudVar(stack[i][1])) {
+                    opcode += 'cloud:';
+                }
+            }
+
             // Add to block list
-            result.push(stack[i][0]);
+            result.push(opcode);
 
             // Don't pull in params from procedures
-            if (stack[i][0] === 'procDef') continue;
+            if (opcode === 'procDef') continue;
 
             // Move to next item and walk
             walk(stack[i].slice(1));
@@ -154,6 +181,35 @@ const extensions = function (project) {
     return result;
 };
 
+/**
+ * Extracts cloud variable information.
+ * @param  {object} project Project object (SB2 format)
+ * @param  {array}  names   Names of all variables in project
+ * @return {object}         Cloud variable information
+ */
+const cloud = function (project, names) {
+    const obj = [];
+
+    // Extract "isPersistent" parameter from all variables in project
+    const cloudyness = extract(project, 'variables', 'isPersistent').id;
+
+    // Ensure that variable names and isPersistent parameter list are the same
+    // length
+    if (names.length !== cloudyness.length) return -1;
+
+    // Iterate over isPersistent values, and extract names of any that are true
+    for (let i in cloudyness) {
+        if (cloudyness[i]) {
+            obj.push(names[i]);
+        }
+    }
+
+    return {
+        count: obj.length,
+        id: obj
+    };
+};
+
 /**
  * Analyzes a project and returns summary information about the project.
  * @param  {object}   project  Project object (SB2 format)
@@ -171,6 +227,8 @@ module.exports = function (project, callback) {
         costumes: extract(project, 'costumes', 'costumeName', 'baseLayerMD5')
     };
 
+    meta.cloud = cloud(project, meta.variables.id);
+
     // Sprites
     meta.sprites = sprites(project);
 
diff --git a/lib/sb3.js b/lib/sb3.js
index b9c910c..811bfd5 100644
--- a/lib/sb3.js
+++ b/lib/sb3.js
@@ -21,9 +21,14 @@ const variables = function (targets, attribute) {
     let occurrences = 0;
     let idList = [];
 
+    // Cloud variables are a type of variable
+    const isCloud = (attribute === 'cloud');
+    if (isCloud) attribute = 'variables';
+
     for (let t in targets) {
         for (let a in targets[t][attribute]) {
             const variable = targets[t][attribute][a];
+            if (isCloud && (variable.length !== 3 || !variable[2])) continue;
             occurrences++;
             idList.push(variable[0]);
         }
@@ -67,10 +72,37 @@ const blocks = function (targets) {
     // Storage object
     let result = [];
 
+    /**
+     * Determine if a argument is the name of a known cloud variable.
+     * @param  {string} arg Argument (variable name)
+     * @return {boolean}    Is cloud variable?
+     */
+    const isArgCloudVar = function (arg) {
+        // Validate argument
+        if (typeof arg !== 'string') return false;
+
+        // Check first target (stage) to determine if arg is a cloud variable id
+        const stage = targets[0];
+        if (typeof stage.variables[arg] !== 'undefined') {
+            return stage.variables[arg].length === 3 && stage.variables[arg][2];
+        }
+    };
+
+    // Iterate over all targets and push block opcodes to storage object
     for (let t in targets) {
         for (let a in targets[t].blocks) {
             const block = targets[t].blocks[a];
-            if (!block.shadow) result.push(block.opcode);
+
+            // Get opcode and check variable manipulation for the presence of
+            // cloud variables
+            let opcode = block.opcode;
+            if (opcode === 'data_setvariableto' || opcode === 'data_changevariableby') {
+                if (isArgCloudVar(block.fields.VARIABLE[1])) {
+                    opcode += '_cloud';
+                }
+            }
+
+            if (!block.shadow) result.push(opcode);
         }
     }
 
@@ -97,6 +129,7 @@ module.exports = function (project, callback) {
     const meta = {
         scripts: scripts(project.targets),
         variables: variables(project.targets, 'variables'),
+        cloud: variables(project.targets, 'cloud'),
         lists: variables(project.targets, 'lists'),
         comments: extract(project.targets, 'comments'),
         sounds: extract(project.targets, 'sounds', 'name', 'md5ext'),
diff --git a/package.json b/package.json
index 3397c6a..a654aba 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "scratch-analysis",
-  "version": "1.0.1",
+  "version": "2.0.0",
   "description": "Analysis tool for summarizing the structure, composition, and complexity of Scratch programs.",
   "main": "lib/index.js",
   "directories": {
@@ -17,7 +17,7 @@
   "author": "Scratch Foundation",
   "license": "BSD-3-Clause",
   "dependencies": {
-    "scratch-parser": "4.3.3"
+    "scratch-parser": "4.3.6"
   },
   "devDependencies": {
     "babel-eslint": "^10.0.1",
diff --git a/test/fixtures/sb2/cloud.sb2 b/test/fixtures/sb2/cloud.sb2
new file mode 100644
index 0000000..bcab352
Binary files /dev/null and b/test/fixtures/sb2/cloud.sb2 differ
diff --git a/test/fixtures/sb2/cloud_complex.sb2 b/test/fixtures/sb2/cloud_complex.sb2
new file mode 100644
index 0000000..976474e
Binary files /dev/null and b/test/fixtures/sb2/cloud_complex.sb2 differ
diff --git a/test/fixtures/sb2/cloud_opcodes.sb2 b/test/fixtures/sb2/cloud_opcodes.sb2
new file mode 100644
index 0000000..da1ab6f
Binary files /dev/null and b/test/fixtures/sb2/cloud_opcodes.sb2 differ
diff --git a/test/fixtures/sb3/cloud.sb3 b/test/fixtures/sb3/cloud.sb3
new file mode 100644
index 0000000..60d584a
Binary files /dev/null and b/test/fixtures/sb3/cloud.sb3 differ
diff --git a/test/fixtures/sb3/cloud_complex.sb3 b/test/fixtures/sb3/cloud_complex.sb3
new file mode 100644
index 0000000..c6522c9
Binary files /dev/null and b/test/fixtures/sb3/cloud_complex.sb3 differ
diff --git a/test/fixtures/sb3/cloud_opcodes.sb3 b/test/fixtures/sb3/cloud_opcodes.sb3
new file mode 100644
index 0000000..77e2158
Binary files /dev/null and b/test/fixtures/sb3/cloud_opcodes.sb3 differ
diff --git a/test/unit/cloud.js b/test/unit/cloud.js
new file mode 100644
index 0000000..793ad9d
--- /dev/null
+++ b/test/unit/cloud.js
@@ -0,0 +1,79 @@
+const fs = require('fs');
+const path = require('path');
+const test = require('tap').test;
+const analysis = require('../../lib/index');
+
+const sb2 = fs.readFileSync(
+    path.resolve(__dirname, '../fixtures/sb2/cloud.sb2')
+);
+const sb3 = fs.readFileSync(
+    path.resolve(__dirname, '../fixtures/sb3/cloud.sb3')
+);
+const sb2Complex = fs.readFileSync(
+    path.resolve(__dirname, '../fixtures/sb2/cloud_complex.sb2')
+);
+const sb3Complex = fs.readFileSync(
+    path.resolve(__dirname, '../fixtures/sb3/cloud_complex.sb3')
+);
+
+test('sb2', t => {
+    analysis(sb2, (err, result) => {
+        t.true(typeof err === 'undefined' || err === null);
+        t.type(result, 'object');
+        t.type(result.cloud, 'object');
+        t.equals(result.cloud.count, 1);
+        t.deepEquals(result.cloud.id, ['☁ baz']);
+        t.end();
+    });
+});
+
+test('sb3', t => {
+    analysis(sb3, (err, result) => {
+        t.true(typeof err === 'undefined' || err === null);
+        t.type(result, 'object');
+        t.type(result.cloud, 'object');
+        t.equals(result.cloud.count, 1);
+        t.deepEquals(result.cloud.id, ['☁ baz']);
+        t.end();
+    });
+});
+
+test('sb2 complex', t => {
+    analysis(sb2Complex, (err, result) => {
+        t.true(typeof err === 'undefined' || err === null);
+        t.type(result, 'object');
+        t.type(result.cloud, 'object');
+        t.equals(result.cloud.count, 8);
+        t.deepEquals(result.cloud.id, [
+            '☁ Player_1',
+            '☁ Player_2',
+            '☁ Player_3',
+            '☁ Player_4',
+            '☁ Player_5',
+            '☁ GameData',
+            '☁ Player_6',
+            '☁ SAVE_DATA2'
+        ]);
+        t.end();
+    });
+});
+
+test('sb3 complex', t => {
+    analysis(sb3Complex, (err, result) => {
+        t.true(typeof err === 'undefined' || err === null);
+        t.type(result, 'object');
+        t.type(result.cloud, 'object');
+        t.equals(result.cloud.count, 8);
+        t.deepEquals(result.cloud.id, [
+            '☁ Player_1',
+            '☁ Player_2',
+            '☁ Player_3',
+            '☁ Player_4',
+            '☁ Player_5',
+            '☁ GameData',
+            '☁ Player_6',
+            '☁ SAVE_DATA2'
+        ]);
+        t.end();
+    });
+});
diff --git a/test/unit/cloud_opcodes.js b/test/unit/cloud_opcodes.js
new file mode 100644
index 0000000..262823b
--- /dev/null
+++ b/test/unit/cloud_opcodes.js
@@ -0,0 +1,59 @@
+const fs = require('fs');
+const path = require('path');
+const test = require('tap').test;
+const analysis = require('../../lib/index');
+
+const sb2 = fs.readFileSync(
+    path.resolve(__dirname, '../fixtures/sb2/cloud_opcodes.sb2')
+);
+const sb3 = fs.readFileSync(
+    path.resolve(__dirname, '../fixtures/sb3/cloud_opcodes.sb3')
+);
+
+test('sb2', t => {
+    analysis(sb2, (err, result) => {
+        t.true(typeof err === 'undefined' || err === null);
+        t.type(result, 'object');
+        t.type(result.blocks, 'object');
+        t.type(result.blocks.id, 'object');
+        t.deepEquals(result.blocks.id, [
+            'whenGreenFlag',
+            'doForever',
+            'setVar:to:',
+            'randomFrom:to:',
+            'changeVar:by:',
+            'setVar:to:',
+            'randomFrom:to:',
+            'changeVar:by:',
+            'setVar:to:cloud:',
+            'randomFrom:to:',
+            'changeVar:by:cloud:',
+            'wait:elapsed:from:'
+        ]);
+        t.end();
+    });
+});
+
+test('sb3', t => {
+    analysis(sb3, (err, result) => {
+        t.true(typeof err === 'undefined' || err === null);
+        t.type(result, 'object');
+        t.type(result.blocks, 'object');
+        t.type(result.blocks.id, 'object');
+        t.deepEquals(result.blocks.id, [
+            'event_whenflagclicked',
+            'control_forever',
+            'control_wait',
+            'data_setvariableto',
+            'data_setvariableto',
+            'data_setvariableto_cloud',
+            'operator_random',
+            'operator_random',
+            'operator_random',
+            'data_changevariableby',
+            'data_changevariableby',
+            'data_changevariableby_cloud'
+        ]);
+        t.end();
+    });
+});